Iris Task/Attempt v2 语义契约
September 10, 2026 · View on GitHub
状态:v0.1.3 已实现;v0.1.4 Host/Provider Adapter、Host Doctor 与作品库 v0 已接入。 Task v2 已覆盖图片、视频、转写与 TTS,包括同步/异步、上传/提交、混合协议候选、工作台稳定用户态、状态流防倒退、重新观察、重新交付、提醒已读/恢复和知情人工重试。作品库 v0 只分离本地作品生命周期;正式 Artifact Manifest 仍属于后续工作。
1. 为什么需要 v2
媒体生成可能在供应商侧产生费用。网络断开并不能证明请求没有被受理,轮询失败也不能证明远端任务失败,下载失败更不能抹掉已经成功的生成结果。Iris 必须分别记录“有没有提交”“供应商是否受理”“远端结果是什么”“是否仍在观察”“产物是否已取回”和“取消是否得到确认”。
本契约使用以下规范词:必须表示不可违反的安全或事实约束;应该表示除非有记录充分的理由,否则需要遵守;可以表示可选行为。
2. 核心对象
- Task:一次用户意图。无论尝试过多少供应商,一个用户请求只能创建一个 Task。
- Attempt:Task 针对一个
providerId::modelId的一次提交尝试。Attempt 必须在可能发送计费请求前持久化。 - 受理证据:远端任务 ID、同步成功响应,或供应商明确声明已受理的其他稳定标识。
- 观察:对已受理远端任务进行轮询或恢复接管,不等同于再次提交。
- 交付:把远端结果下载、校验并原子保存为 Iris 本地产物。
Task 与 Attempt 使用稳定 ID。Attempt 的 model 与 Task 的权威 modelRef 必须保存复合身份 providerId::modelId;迁移期 Task 仍可额外保留裸 model 供 v0.1.2 UI 读取,但新逻辑不得以该兼容字段判断模型身份。
3. 正交状态
Task v2 至少包含以下事实轴;字段值采用可序列化的 snake_case 英文枚举:
| 字段 | 值 | 含义 |
|---|---|---|
phase | queued · submitting · accepted · running · terminal | Iris 当前是否还会自动推进该任务 |
acceptance | none · not_accepted · accepted · unknown | 当前/最终 Attempt 的远端受理事实 |
watchState | idle · active · suspended · exhausted | Iris 是否仍在观察远端任务 |
outcome | none · succeeded · failed · canceled · unknown | 远端生成或确定性本地动作的结果 |
deliveryState | none · pending · downloading · ready · failed | 成功结果是否已经可靠落成本地产物 |
cancelState | none · requested · remote_confirmed · local_confirmed · unknown | 取消请求及其确认边界 |
status 仅为旧消费者保留,不再是真相来源。新代码必须读取上述字段。
3.1 旧 status 的保守派生
| v2 事实 | 旧 status |
|---|---|
outcome=succeeded 且 deliveryState=ready | succeeded |
outcome=failed | failed |
outcome=canceled 且取消已由远端或确定性本地动作确认 | canceled |
| 其余,包括受理未知、观察耗尽和交付失败 | running |
四态旧字段无法无损表达“需要人工处理”。派生时宁可保持非终态,也不得伪造失败、成功或取消。现代 UI 必须使用 v2 事实轴。
4. 受理与自动 failover
自动 failover 的许可只由受理事实决定:
acceptance | 允许自动切换 Provider | 说明 |
|---|---|---|
none | 否 | 尚未形成最终分类,必须先收敛为明确状态 |
not_accepted | 是 | 已确认计费/生成请求没有被供应商受理 |
accepted | 否 | 已有受理证据,再次提交可能重复计费 |
unknown | 否 | 请求可能已经被受理,必须保守停止 |
以下信息都不能单独授权自动 failover:retryable=true、HTTP 429/500、超时、连接重置、JSON 解析失败、轮询失败、下载失败、本地持久化失败或事件发布失败。
4.1 分类规则
- 本地参数校验失败、请求发送前明确中止,或供应商明确拒绝且确认未创建任务:
not_accepted。 - 收到远端任务 ID、同步成功响应或等价证据:
accepted。 - 请求可能已经离开本机,但响应在获得受理证据前丢失:
unknown。 - 无法证明“未受理”时,必须选择
unknown,不得为了可用性猜测not_accepted。
如果供应商支持幂等键,Attempt 应记录并复用该键;幂等能力可以帮助查询或安全恢复,但在未经合约证明前,不能把“发了幂等键”等同于“可以随意重试”。
最小 Provider 返回结构、写前/写后 Hook 和错误字段详见Provider 提交契约 v0。
5. Attempt 写前记录
每次 Attempt 必须在可能发送生成请求前至少保存:
{
"id": "attempt_...",
"ordinal": 1,
"providerId": "provider-a",
"model": "provider-a::model-a",
"acceptance": "none",
"stage": "preparing",
"idempotencyKey": "optional-provider-key",
"startedAt": "2026-09-06T00:00:00.000Z"
}
上传临时输入与提交生成任务必须是不同 stage。上传成功并不等于生成请求已受理;但上传、提交和响应解析的错误证据都必须保留为脱敏分类,而不是一段不可机读的字符串。
6. 远端结果与产物交付
- 远端返回成功后立即记录
outcome=succeeded,然后进入deliveryState=pending/downloading。 - 下载、校验或本地落盘失败时,保持
outcome=succeeded,写入deliveryState=failed。 - 交付失败只允许重新下载/落盘,不允许重新生成。
- 只有本地产物完成原子落盘并可读后,才写
deliveryState=ready。 - 供应商明确返回生成失败时才写
outcome=failed。 - 任务历史与本地作品是两个生命周期:删除终态任务不得隐式删除作品,删除作品也不得改写任务事实;媒体是否仍可访问由作品库单独呈现。
7. 观察、超时与恢复
- 单次或连续轮询失败只改变观察事实;不得据此写
outcome=failed。 - 达到本地盯守上限时写
watchState=exhausted,结果通常为outcome=unknown。 - 插件关闭或进程退出导致观察停止时,应保留可恢复信息;重启只能恢复观察,不能重新提交已受理或受理未知的 Attempt。
accepted的异步 Attempt 若供应商配置暂不可用,应进入watchState=suspended并给出修复建议,不能伪造远端失败。unknown且没有远端任务 ID 时,只能等待供应商查询能力、幂等查询或用户知情决策。
8. 取消
- 用户请求取消时先写
cancelState=requested。 - 供应商明确确认取消后,写
cancelState=remote_confirmed与outcome=canceled。 - 纯本地、尚未产生远端副作用的动作确定停止后,可以写
cancelState=local_confirmed与outcome=canceled。 - 仅停止本地轮询不等于远端取消。
- 取消请求超时、断网、供应商不支持取消或结果无法确认时,写
cancelState=unknown;outcome也不得伪造为canceled。
9. 错误与隐私
结构化错误至少应区分 stage、category、HTTP 状态、供应商错误码、受理状态和可安全展示的信息。任务、Attempt、事件与日志不得保存 API Key、Authorization 头、完整请求体、私有绝对路径或带临时签名的结果 URL。
错误是否“可重试”与是否“允许自动重新提交”是两个不同问题。前者描述技术性质,后者只能由 acceptance=not_accepted 授权。
10. 事件与 UI
- 每次 Task v2 持久化变更得到单调递增的 Task
revision。完整状态快照另带进程stateEpoch/stateRevision;客户端拒绝同一 epoch 内迟到的旧快照,SSE 重连后以持久化全量快照收敛。 - SSE 慢消费者进入背压时只保留最新待发快照,避免旧状态排队造成内存增长或视觉倒退。
- UI 应把事实映射成“排队中、运行中、观察暂停、需要确认、结果待取回、已完成、明确失败、已取消”等人类状态,不应在手机界面直接堆叠所有内部字段。
acceptance=unknown、cancelState=unknown和deliveryState=failed必须提供明确的人工接管入口;任何可能再次计费的操作都需要单独确认。- 提醒处置与任务事实正交:标为已读只把任务移出主动提醒队列,恢复提醒只重新开放该队列;两者都不得修改受理、观察、结果、交付或取消事实。
- 知情重试创建并关联新 Task 后,原提醒自动记录为“已通过重试处理”。原任务保留原始未知事实和审计记录,但不继续产生重复角标;用户可从历史详情恢复提醒。
11. 兼容与迁移
- v0.1.x 只允许向现有 JSON 记录增加字段,不执行破坏性批量迁移,不顺带引入 SQLite/JSONL。
- 旧记录缺少 v2 字段时,由纯兼容规范化器生成内存视图:既有成功记录保留成功证据;转存失败映射为远端成功但交付失败;轮询失败、无远端 ID 的运行记录和旧取消记录映射到
unknown,不能猜测失败、未受理或远端已取消。 - 读取旧任务不应立即重写整个文件;只有真实更新发生时才按原子写规则保存扩展后的记录。
- 在 v2 UI 和消费者完成迁移前继续输出派生
status。
12. 必须通过的故障证据
故障注入至少覆盖:请求发送前失败、提交响应丢失、获得远端 ID 后本地落盘失败、轮询 429/500/超时、远端成功后下载失败、取消响应丢失,以及 submitting/accepted/running 阶段的进程退出与恢复。
每个场景必须同时断言最终事实字段和供应商提交调用次数。对于 accepted 或 unknown,自动重复提交次数必须为 0。
13. 本版非目标
- 不在语义冻结阶段重写全部任务存储或 UI。
- 不新增 Gemini、Fal、Replicate 等供应商分支。
- v0.1.3 包含零网络、可独立运行的离线 Doctor v0 与最薄 CLI;v0.1.4 已接入 Host/Provider Adapter、Host Doctor 和不含谱系的作品库 v0。完整 Artifact Manifest 仍属于后续版本。
- 不用更多自动重试换取表面成功率。