ARTICLE · 1299870
【AX 实战 06】官方文档说 AX 有四种 kind,我数了三遍只有三个
副标题:三个 kind 加一个作用域:Task / Workspace / Model / Atespace
上期回顾:第 5 章我们跑通了第一个 Task 的完整闭环——apply 完停在 Suspended(P0-1)、ax resume 才真正拉起 worker、ssh 要过 Running + debug: true 两道门槛。你可能已经写过好几份 YAML 了。这一章退后一步,把 YAML 里的「层」看清:docs/manifests.md 开头说 AX 有「四种 kind」,我照着数了三遍,只有三个。
本篇要点(5 行)
「四种 kind」是文档笔误:可 apply 的 kind 只有 Task / Workspace/Model三个,第四个「atespace」是每个资源的metadata字段(F2)。Task是最小隔离单元:创建后不可变,一个 Task 可以是整份活,也可以是一棵任务树的根。状态分两层读: phase是一行摘要,Ready/WorkspaceReady两个 condition 才是细节;suspend 时Ready=False、reason 就是 Suspended,而WorkspaceReady翻上去就不再翻回来。Workspace声明一次、多处绑定、只准备一次(marker 机制);Model把凭证留在 Kubernetes Secret 里,配置一次处处引用。Atespace是作用域,系统自动创建(EnsureAtespace),你永远不需要 apply 它——顺带说清 Sandbox归谁管(F3)。
和多数 AX 文章不同:这一章正面纠正官方文档的笔误,并给出四条源码证据;同时把 phase/condition 的翻转时机讲到源码级,让你看 describe 的 Conditions 表时知道每一行在说什么。
目录锚点:① 纠错:四种 kind → ② Task → ③ phase 与 condition → ④ Workspace → ⑤ Model → ⑥ Atespace → ⑦ 一 Task 一 Workspace + 隔离归谁管 → ⑧ 看 YAML 判层次 → ⑨ 失败复盘 → ⑩ 一页速查
前置与难度:前置第 5 章(能 apply/resume/ssh 一个 Task)| ★2 | 约 15 分钟 | 不需要额外环境,对照源码读即可
金句:atespace 不是 kind,是户口。
一、先纠一个错:「四种 kind」是笔误(F2)
打开 docs/manifests.md,第一句话就是:
All four kinds can live in one multi-document YAML file.「四种 kind 可以写进同一份多文档 YAML」。听起来很顺。但你把 examples/task.yaml 打开数一数:kind: Task、kind: Workspace、kind: Model——三个。
是例子少写了一种吗?不是。是文档笔误。四条源码证据:
ax.proto:61/130/189 | |
types.go:34-36 | |
main.go:355 | |
ax.proto:53 |
一条条看。pkg/apis/v1alpha1/ax.proto 里定义的资源消息恰好三个:Task(61 行)、Workspace(130 行)、Model(189 行),其余全是 Request/Response。pkg/apis/v1alpha1/types.go 的常量区写着:
const ( KindTask = "Task" KindWorkspace = "Workspace" KindModel = "Model")三个,没有第四个。再看 CLI 自己怎么说——cmd/ax/main.go:355,给它喂一个不存在的 kind,报错原文:
unsupported kind "Foo" (expected Task, Workspace,or Model)括号里列了三种。CLI 是最后一道关,它不接受第四种。
那第四个「kind」指什么?大概率指 atespace。但看 ax.proto:53:
message ObjectMeta { string name = 1; string atespace = 2; google.protobuf.Timestamp create_time = 3;}atespace 是 ObjectMeta 的第二个字段——每个资源的「户口」,跟 name 同级。它有自己的创建逻辑(见第六节),但它不占一个 kind 席位。
自验(在 AX 仓库根目录跑,输出恰好三行):
grep -n "KindTask\|KindWorkspace\|KindModel" \ pkg/apis/v1alpha1/types.go# 34: KindTask / 35: KindWorkspace / 36: KindModel# 三个,没有第四个一句话记法:三个资源,一个作用域。 后面六个小节把这四样逐个讲透。
二、Task:最小隔离单元
2.1 定义:便宜到可以随手扔
docs/concepts.md 的原话:The smallest unit of isolated execution(最小的隔离执行单元)。它声明四样东西:镜像与命令、资源上限、环境变量、(可选的)Workspace 引用。
为什么设计得这么「小」?概念文档解释得很坦白:Agent 不是「一个进程跑到底」,它的一生是规划、委派、重试、扇出(把一个任务拆成许多并行的子任务)。AX 不去模拟这个形状,只给一个便宜到可以随手创建、随手挂起、随手扔掉的原语,让 Agent 自己按需组合。
2.2 创建后不可变:两个条件的判定
这是很多人踩的第一堵墙。ax apply 一份 Task 后想改个 env 再 apply?看 internal/server/server.go:174-177 的精确判定:
if existing.Status.Phase != PhasePending || !proto.Equal(existing.Spec, task.Spec) { // reject: already exists and is immutable}翻译成人话:已存在的 Task,只要「phase 不是 Pending」或「spec 有改动」任一成立,当场拒,报错原文:
task "task123" already existsand is immutable两个推论:
- 想改 spec
:只能 delete 再 apply。注意不可变的是 spec—— status(phase、conditions)当然在持续变化,否则第 5 章的状态流就不成立了。 - 幂等重试的口子
:Task 卡在 Pending(比如 Substrate 暂时不可用)时,同 spec 再 apply 一次会被接受,沿用原对象继续 provision。这是留给故障恢复的通道——除了这个口子,重 apply 一律被拒(哪怕 spec 一模一样)。
对比之下 Workspace 和 Model 都有 Update 接口(第四、五节),可以原地改。Task 是三者里唯一不可变的。第 7 章把这条判定展开成完整的校验两栏。
2.3 可被 Agent 扇出成树
概念文档里有一段值得抄下来:
A single task may be the whole job, or it may be the root of a large tree of tasks spawned as the agent breaks the problem down.
一个 Task 可以是整份活,也可以是一棵任务树的根。Agent 把大问题拆小,每个小问题开一个 Task——每个节点拿到同样的沙箱、同样的生命周期、同样的工具。第 5 章你跑的 task123 是单节点;真实 Agent 场景里它可能有几十个子节点。
三、phase 与 condition:状态的两层
3.1 phase:一行摘要
status.phase 一个词说清 Task 在哪:Running、Suspended、Failed、Terminating、Pending、Resuming、Suspending、Unknown。
这八个里有一条重要的分界线(types.go 注释 + server.go 的 setActorStatus):
- AX 自己维护的
: Pending/Failed/Terminating——AX 的权威状态,调谐失败、删除请求由 AX 写入。 - 跟着 Actor 走的
: Running/Resuming/Suspending/Suspended/Unknown——从 Substrate 的 Actor 状态映射而来,以 Actor 实况为准:每次读取时setActorStatus用 Actor 当前状态覆盖它们。
源码里有个小函数 IsAXPhase 专门做这个判断(types.go:124-130)。为什么这么分?Actor 是 Substrate 的事实,AX 只是读;把映射状态当成 AX 自己的权威数据反而会和 Substrate 打架。
3.2 condition:两行细节
phase 是摘要,细节在 conditions。只有两个 type(types.go:73-75):
WorkspaceReady | |
Ready | WorkspaceReady=True |
注意 Ready 的定义把 WorkspaceReady 包含在内——所以等就绪,盯 Ready 一个就够(概念文档原话:This is the one to wait on)。
ax describe task 的输出长这样(示例输出,格式依据 cmd/ax/main.go):
Conditions: TYPE STATUS REASON MESSAGE Ready True TaskRunning WorkspaceReady True SetupComplete两行全绿 = 真就绪。只有一行绿 = 还在初始化。
3.3 suspend / resume 时,Ready 怎么翻
这是本节的正题。翻转规则(docs/concepts.md:17-18 + internal/server/server.go 的 setActorStatus):
- suspend
: Ready→False,reason 就是 phase 本身——Suspended。消息形如「Actor is suspended」。 - resume 成功
: Ready→True,reasonTaskRunning。 WorkspaceReady不翻。挂起再恢复,它保持 True。
第 3 条的源码注释值得看(reconciler.go:370-371):
// setNotReady marks the task's Ready condition False.// WorkspaceReady is left untouched: workspace setup is// a one-time step whose result outlives actor failures// and suspends.workspace 只初始化一次,它的成果比 Actor 的挂起/失败活得久。所以 resume 回来时,Ready 的判定只看 Actor 是否 running——workspace 早就 ready 了。
这里有个源码级细节可以帮你看懂 describe:types.go 的注释说「Ready 因 Actor 不在跑而为 False 时,reason 用 task 的 phase 代替」。setActorStatus 的实现确实这么干——reason 直接填 phase 值。所以你看到 reason 列写着 Suspended、Resuming、Failed 时,那是 phase 在替 Ready 说话。
到这里,状态层就讲完了:phase 是摘要,conditions 是细节,reason 列还藏着 phase 的影子。下一节把「环境」这个层次铺开。
四、Workspace:声明一次,多处绑定
4.1 它准备的是「干活前的桌面」
Agent 真正干活之前,得先把桌面铺好:仓库克隆到正确的分支、允许调用的 MCP 工具接上、技能包放到位。每个任务都重复这套准备,每个 Agent 框架都重新发明一遍。Workspace 就是把这套准备声明化、只做一次。
WorkspaceSpec 四类内容(ax.proto:137-142):
git | |
files | AGENTS.md) |
mcpskills |
细节(git 的 depth、MCP 的 registries vs servers、skills 的落点)第 8 章深潜。本章只建立定位:Workspace 是环境,Task 是干活的人。环境铺一次,人可以来很多个。
4.2 只准备一次:marker 机制
「只准备一次」怎么落地?看 internal/workspace/setup.go。Runner 在沙箱里第一次启动时执行 maiden run(少女航程):克隆仓库、挂载文件、装 MCP 和 skills。成功后写一个 marker 文件:
InitializedMarkerFilename = "initialized"下次启动先查 marker(setup.go:85-87):
workspace already initialized;skipping maiden run setup有 marker 就跳过整段准备。这就是 suspend → resume 之后你的 /workspace 还在、不用重新 clone 的原因——快照连 marker 一起带回来了。
反过来,准备失败会怎样?看 setup.go:102 的日志:
maiden run workspace setup completedwith errors; marker omitted to allow retry失败不写 marker,下次启动从头再来。这是 F11 的例外(文档说「只准备一次」,实际失败会重试),第 8 章细讲重试次数与间隔。
五、Model:配置一次,处处引用
5.1 四个字段说清一个 Model
ModelSpec(ax.proto:196-204)四个字段:
provider | google / anthropic) |
model | gemini-3.8-flash) |
secretKey | |
parameters |
关键设计在 secretKey——它是引用,不是明文。指向一个 K8s Secret 的 name + key:
spec: provider: google model: gemini-3.8-flash secretKey: name: gemini-api-secret key: GEMINI_API_KEYAPI key 躺在 Secret 里,Task 的 YAML、env、describe 输出都碰不到它。换 key 轮换时改 Secret 或重新 apply 一条 Model,不用翻所有 Task。为什么这样设计?第 9 章从 F16(config 明文会被 describe 打印)讲起,这里先记住结论:密钥走引用,永远别进 Task。
一个预告(F14):parameters 的键名以 provider 为准。proto 注释举例 maxOutputTokens(Gemini 风格),而 docs/manifests.md 的示例写 maxTokens(Anthropic 风格)——AX 不校验键名,原样透传。照抄错键名不会报错,只会静默失效。第 9 章细辨。
5.2 一句话定位
Model 是平台配置,不只是任务配置——AX 自身组件也读 Model 资源。所以它值得单独管理,而不是塞进某个 Task 的环境变量里。第 9 章从密钥轮换的完整流程讲起。
六、Atespace:不是 kind 的那一个
6.1 它是作用域,自动创建
回到第一节的结论:atespace 是 ObjectMeta 的字段。概念文档第一句:Every AX resource lives in an atespace(每个 AX 资源都住在某个 atespace 里)。默认值 default。
它对应到 Substrate 就是 namespace 一样的隔离边界。你在 YAML 里写:
metadata: name: task123 atespace: team-a或者用旗标 ax apply -a team-a -f m.yaml。两者冲突会被当场拒绝(main.go 的 setApplyAtespace):
metadata.atespace "default" does notmatch --atespace "team-a"6.2 你永远不需要 apply 它
重点来了。reconciler.go:146,每个 Task 调谐时第一件事就是:
if err := r.client.EnsureAtespace( ctx, atespace); err != nil {EnsureAtespace——存在就用,不存在就建。用户没有 kind: Atespace 可写,apply 一个进去只会收到第一节那条 unsupported kind 报错。作用域随用随建,这是它跟三个 kind 的行为差异。
命名规则跟着 Substrate 资源名走(docs/manifests.md):RFC 1123 小写标签,≤63 字符,字母数字开头结尾。metadata.name 和 metadata.atespace 都受这条约束,违反的 apply 当场拒,不让它烂在后面的 Actor 创建里。
6.3 多 atespace 什么时候用
一个团队一个作用域:team-a / team-b 各自隔离资源名,互不踩。第 22 章讲多租户时展开。单人单集群用默认的 default 就好,不必发明复杂度。
到这里,四样东西各归其位:三个 kind 管资源,一个 atespace 管户口。剩下的问题只有一个——隔离到底归谁管?
七、为什么一个 Task 只绑一个 Workspace + 隔离归谁管
7.1 一个 Task = 一个工作目录
TaskSpec 里的 workspace 引用是单数的(ax.proto:106-109):
message WorkspaceRef { string name = 1; string path = 2;}name + path,没有列表。为什么?因为 workspace 挂载的路径就是命令的工作目录(默认 /workspace,binding 可改)。一个进程只有一个 cwd。要两套环境?开第二个 Task——可以共享同一个 Workspace,也可以各挂各的,这正好回到 2.3 的扇出模型。
Workspace 的「多处绑定」指的是一份 Workspace 被多个 Task 引用(name: default-workspace 写在每个 Task 的 spec 里),跟「一个 Task 绑多个 Workspace」是两回事。
7.2 F3:Sandbox 不归 AX 管
既然讲层次,顺手拆掉一个高频混淆。AX 的路线图(docs/roadmap.md:12)里有 Sandbox / SandboxConfig,但它标注的是「待稳定的 spec」——AX proto 里没有任何 Sandbox 类型,CLI 也不接受。
同名的 SandboxConfig 在 Substrate 那边是真实 CRD,e2e 里正在用(gvisor-default)。同名不同层:
写作与问答口径:AX 没有 Sandbox 原语;隔离后端由 Substrate 的 SandboxConfig 决定。 不要说「AX 的 Sandbox 资源」,熟悉 Substrate 的读者会当场纠正。
八、完整案例:看到任何 YAML,说出它落哪一层
本章的产出是「一眼判层」的能力。给你一份混合 YAML 示意(结构对齐 examples/task.yaml 的三段,字段已简化,不可直接 apply),练习给每段判层:
kind: Taskmetadata: {name: task123, atespace: default}# → 层:kind #1。最小隔离单元,建后不可变---kind: Workspacemetadata: {name: ws1, atespace: default}# → 层:kind #2。环境,可 Update,只准备一次---kind: Modelmetadata: {name: m1, atespace: default}# → 层:kind #3。凭证引用,可 Update三个 --- 分隔的文档,三个 kind。它们共同的 metadata.atespace: default 是户口——作用域,不是第四段文档。
三步判层法(拿到任何 AX YAML 都适用):
看 kind:——只可能是Task/Workspace/Model之一;见到别的当场知道是笔误或错平台。看 metadata.atespace——它决定资源落在哪个作用域;不写则默认default,旗标冲突会被拒。看 spec归属——镜像/命令/资源归 Task,git/files/mcp/skills 归 Workspace,provider/secretKey 归 Model。放错位置 apply 会被拒不解释(下一章讲校验)。
九、失败复盘:三个真会踩的坑
坑一:apply 一个 kind: Atespace
现象:想显式建作用域,写了第四段文档。原始报错:
unsupported kind "Atespace"(expected Task, Workspace, or Model)原因:atespace 自动创建(EnsureAtespace),根本没有这个 kind。解法:删掉那段文档,用 metadata.atespace 或 -a 指定作用域。
坑二:想改 Task 的 image 再 apply
现象:改完 YAML 再 apply。原始报错:
task "task123" already existsand is immutable原因:Task 的 spec 不可变(server.go:176)。解法:ax delete task task123 后重新 apply。想原地改的资源是 Workspace / Model(它们有 Update 接口)。
坑三:在 AX 里找 Sandbox 资源
现象:按 docs/roadmap.md 的条目 kubectl get sandboxconfig -n ax-system,空或报错。原因:SandboxConfig 是 Substrate 的 CRD,不在 AX 这层。解法:装好 Substrate 后查它的 namespace(e2e 里能看到 gvisor-default);AX 侧只需知道隔离后端不归你配。
十、反例 / 正例
反例(照文档数 kind):「四种 kind 都能写进一份 YAML」→ 找第四种找不到 → 以为自己漏了什么 → 翻遍文档浪费半小时。
正例(本章建立的判层法):数 kind 看 types.go 三个常量 → CLI 报错文本自证 → atespace 归 metadata → Sandbox 归 Substrate。每层归属有源码出处,不靠猜。
十一、自测 3 题
docs/manifests.md说「四种 kind」,实际有几个?第四个「kind」的真实身份是什么?(提示:F2 + ax.proto:53)ax suspend之后, Ready和WorkspaceReady各自怎么变?reason 列会写什么?(提示:phase 代替 reason)为什么一个 Task 只能绑一个 Workspace?想给一个任务两套环境该怎么办?(提示:cwd + 扇出)
十二、一页速查
ReadyWorkspaceReady) | |
一句话小结
三个资源,一个作用域,看 YAML 先数 kind 再看户口。
十三、下期预告
第 7 章|Manifest 校验:哪些会被 apply 拒绝,哪些会变成 Failed
层次看清了,下一章进校验。同样是「提交被拒」,有的当场拒(命名、config 超限),有的 apply 成功却慢慢变成 Failed(tag 镜像、Substrate 拒绝的 limits)——两栏分清,一次 apply 成功不靠试错。
金句:atespace 不是 kind,是户口。
互动
这一章讲的是「四个层次各自归位」。
先做一个动作:打开 examples/task.yaml,给三段文档各标一行注释——它的 kind 是什么、住在哪个 atespace、spec 里放的是哪类东西。30 秒标完,判层能力就上身了。
然后说最让你意外的一点:是「官方文档真的写错了」(F2)、是「Task 居然建后不可变」,还是「suspend 后 WorkspaceReady 不翻」?评论区聊聊,我挑几个在第 7 章讲校验时专门回应。
置顶补充:读源码有个习惯——先找常量,再找报错文本。常量定义边界(三个 Kind),报错文本是最后的自证。这两个位置比叙述性文档可靠。
事实锚点:AX
4b7bf6f/ Substratev0.6.0适用边界:基于上述锚点撰写;AX 为 pre-1.0,行为可能漂移,以你实际部署的版本为准。
附:标题候选库(留档,供 §6.4 多平台改编复用)
★ 公众号推荐标题(系列统一定稿):【AX 实战 06】官方文档说 AX 有四种 kind,我数了三遍只有三个 副标题(技术定位):三个 kind 加一个作用域:Task / Workspace / Model / Atespace
| 钩子型 | |
| 利益型 | |
| 身份代入型 |
副标题(技术定位):三个 kind 加一个作用域:Task / Workspace / Model / Atespace
本文基于 AX 公开信息撰写,事实锚点 AX
4b7bf6f/ Substratev0.6.0(pre-1.0,可能漂移);代码行为以你部署版本为准,争议处以官方文档为最终依据。















































































