Agent 运行配额与任务生命周期治理
按用户从配置、创建、排队、运行到终态诊断的完整流程,说明 OpenProgram 当前已有什么、参考实现怎样限制资源,以及后续需要建立的统一治理契约。
00 范围与最终决策
本设计治理的是 agent/Job 的创建资格、会话内排队与运行容量、父子预算、实际使用量、终止和恢复。现有 agent.max_spawn_depth、agent.max_spawn_fanout、agent.max_messages 继续负责调用拓扑和循环约束,不被重新解释为资源预算。
| 决策 | 结论 | 理由 |
|---|---|---|
max_live_global | 不新增 | 现有进程级 ThreadPoolExecutor 已由 OPENPROGRAM_JOB_WORKERS 控制全局执行容量,默认 4。再增加同义配置会形成两个可能冲突的全局上限。界面只读展示有效 worker capacity。 |
max_live_per_session | 新增 | 防止一个会话占用全部 worker;它只计真正执行且尚未确认停止的任务。 |
max_queued_per_session | 新增 | 限制已 admission 但未运行的任务,避免无界持久队列。排队任务不占 live 名额。 |
max_jobs_per_session | 新增 | 限制会话生命周期内成功 admission 的累计任务数;任务终态后不返还。 |
| token / 成本 / 运行时长 / 空闲时长 | 均纳入,强制条件不同 | token 和时间在具备安全上界与可取消边界时可强制;费用只有价格已知时才可预先强制。未知费用不能按 0 处理。 |
| 事实账本 | 复用 usage ledger | 实际 provider 使用量仍只写 usage_events。admission 和未结算 reservation 是执行状态,不复制第二套实际用量账本。 |
协作计数与资源限制分别计量
协作操作与消息、派生计数的规范见 Agent 协作设计 §5;本页规定持久执行的 admission、容量与资源预算。max_jobs_per_session 不是派生深度或单轮派生数量的别名。现有 Job 的 id 同时是 canonical execution_id;job_output(job_id=...) 保留参数名,但接收同一个执行标识。
| 限制 | 计数范围与触发行为 | 达到上限 |
|---|---|---|
agent.max_spawn_depth | 当前执行上下文的派生代数;只有创建新分支的 agent(...) 将子代数加一。向已有分支派发不增加代数。 | 拒绝本次新建分支;已有分支仍可在其他限制内接收工作。 |
agent.max_messages | 执行路径上的消息计数;spawn、agent(to=...) 和 send_message 将目标计数设为发送方加一。自动回送沿用已完成 Job 的计数,不再加一;同一发送轮次的并行调用不是共享累计计数器。 | 拒绝新的协作投递;不取消已有 Job,不停止当前轮次的其他工具。 |
agent.max_spawn_fanout | 按调用方 (session_id, turn_id) 统计 creates_agent=true 的新分支;它不计向已有分支投递。 | 拒绝本轮新增分支;不因子执行完成而返还本轮数量。 |
max_jobs_per_session | 按运行目标 session(Job 的 parent_session_id)累计成功 admission,包含新分支和已有分支上的新 Job。跨 session 的调用方来源另行记录。 | 拒绝新 admission,返回 quota.jobs_exhausted、retryable=false;已接纳 Job 终态不返还累计数量。 |
max_queued_per_session | 目标 session 中 preparing 与 queued admission 的数量。 | 拒绝新 admission,返回 quota.queue_full、retryable=true;拒绝不创建新的 Job。 |
max_live_per_session / scheduler_capacity | session / 全局 live 与 stopping claim 的数量;尚未确认停止的执行仍占容量。 | 已接纳 Job 保持 queued,等可运行后再取得 live claim;不把容量等待写成执行失败。 |
前三项协作限制中 0 表示不执行该上限检查;消息与代数仍可随执行上下文传播。资源限制使用不同的 schema:null 表示未设置/继承,非空值必须为正数,0 无效。继承后仍未设置的 queued/累计/token/cost/runtime/idle 限制不启用;live 的有效默认值仍由 scheduler capacity 决定。
agent(to=...) 不创建分支,但创建新的执行,因此检查消息限制并申请目标 session 的 admission。send_message 也不创建分支;当前空闲目标路径会通过 run_agent_turn_async 创建 creates_agent=false 的 Job,消费目标 session 的累计 admission。普通消息对忙目标的 inbox 接收发生在创建该执行之前:投递已排队不代表 Job 已获 admission,后续拒绝必须通知发送方;不能把消息接口视为资源限制的豁免入口。
job_output 仍设置 can_use=delegation_budget_left,因此会被消息预算隐藏,这是待修复的接口行为。当前注册的 agent 工具为 agent、list_jobs、job_output;不存在独立 job_stop 工具。取消复用 canonical execution 控制入口及内部 JobRunner.cancel_execution,不能把内部方法名当作可调用模型工具。本设计不实现后台会话 UI、MCP server、购买或充值、组织计费、跨机器集群调度。共享 DTO、reason code、预算 scope 和 reservation 协议可以被这些系统以后复用,但本页不预设其产品交互。
01 当前实施状态:admission、资源账本与执行控制已接入,仍有明确缺口
当前工作树已经把 Job、ResourceGovernor、durable admission、owner lease/reconcile、运行时间边界和资源视图接入主要执行链。源码证据支持这些能力存在;本轮没有重跑产品测试,因此历史验证结果保留在 Git 中,以下只区分源码确认的实现、源码确认的缺口和本轮未重新验证的范围。
| 现有机制 | 已经确认的行为 | 治理缺口 | 证据 |
|---|---|---|---|
| 全局执行池与 admission | OPENPROGRAM_JOB_WORKERS 仍是唯一全局容量;runner 与同步 agent 入口已调用 ResourceGovernor.admit_job(),SQLite 用 BEGIN IMMEDIATE 保护计数。 | JobRunner 已启动 durable dispatcher、reconciler 和 budget loop;本轮未重跑 whole-product gate。 | openprogram/agent/job/runner/ · admit_job_entity / spawn_job / _run_one openprogram/agent/resource_governance/ · admit_job / try_start |
| 任务与资源持久状态 | Job 已有 budget_scope_id、effective_limits、reason_code;usage DB 已有 admissions、budget scopes 与 reservations。 | Job、canonical execution store、ResourceSaga 和 startup recovery 源码已存在;崩溃竞争矩阵本轮未重新验证。 | openprogram/agent/job/types.py openprogram/usage/ledger.py openprogram/agent/resource_governance/ |
| 会话限制 | max_live_per_session、max_queued_per_session、max_jobs_per_session 已进入 config schema,并由 governor 原子检查。 | 当前源码已有 JobResourceView 和 Web/CLI/TUI/model surfaces;入口一致性与公平性回归本轮未重跑。 | openprogram/config_schema.py openprogram/agent/resource_governance/ · resolve_resource_limits / _capacity |
| 取消与恢复 | 取消继续按父子关系级联,运行任务使用 cancel/kill/watchdog;governor 已有 lease、stopping 与 release 原语。 | 当前源码已有 RuntimeControlService、终态恢复、lease reconcile 和 stopping/release 路径;跨进程竞争证据本轮未重新验证。 | openprogram/agent/job/runner/ · cancel_execution / _run_one openprogram/agent/resource_governance/ · renew_lease / request_stop / release_task |
| token / cost | BudgetedRequest 已将公共 provider stream 入口接到 reserve_provider_request,在凭证/网络访问前预占,收紧 output cap,并在终态 usage 到达时结算。unknown cost 仍由 usage ledger 保留来源。 | 生产路径使用 reserve_provider_request,而不是独立的 reserve_tokens/reserve_cost 方法。源码已有 accounting 检查及成本上限下未知价格拒绝;本轮未重跑 provider 覆盖与故障注入 gate。 | openprogram/providers/stream.py · stream_simple / stream openprogram/providers/budget.py · BudgetedRequest openprogram/agent/resource_governance/ · reserve_provider_request |
| runtime / idle | 配置和 budget scope 可以保存 max_runtime_seconds、idle_timeout_seconds;既有 provider/tool timeout 仍独立工作。 | 当前 runner 已有 budget loop、time limits、bounded_operation_timeout 和 non-preemptible guard;各操作类型回归矩阵本轮未重新验证。 | openprogram/config_schema.py openprogram/providers/utils/deadline.py openprogram/agent/job/runner/ |
| 用户状态 | Web 与 runtime control 已有执行查询/取消和状态广播;模型工具提供 list_jobs/job_output;JobResourceView 可生成基础结构。 | 当前源码已有 JobResourceView、job_status resource 投影及 Web/CLI/TUI/model 读取面;各界面一致性测试本轮未重新验证。 | openprogram/agent/resource_governance/ · JobResourceView / build_task_resource_view apps/server/openprogram_server/_webui/ws_actions/job.py openprogram/programs/tools/agents/agent/list_jobs/ |
02 其他项目怎么设计:比较计数单位、拒绝时机、持久性、取消和可见性
参考对象的数字服务于理解其计数单位和运行模型,不构成 OpenProgram 默认值依据。完整协作证据见Agent 协作实现对照,现有内部设计见Agent 协作设计和async-job-lifecycle.zh.html。
| 项目 | 真实计数单位与限制 | 拒绝/阻塞时机 | 持久性与取消 | 用户可见性 | 对本设计的作用 |
|---|---|---|---|---|---|
| Claude Code | 二进制快照显示 spawn depth 3、并发 20、inbox 50;矩阵证据另记录每会话累计派生量。 | 深度/并发在创建路径检查;不同 fork 路径存在“先保留工具、调用时拒绝”的差异。 | agentId 可追踪;已确认级联取消。二进制与泄露源码树不是同一实现。 | list/output/stop 与 agent 状态。 | 说明 live 与累计量需要分开,也说明必须声明证据版本;不采用其数值。 |
| Codex CLI | 全局并发 6;V2 每会话 4;V1 深度 1,V2 计算层数但未强制。 | execution controller 在启动前做并发检查。 | ThreadId;list/wait/interrupt;V2 interrupt 未确认完整级联。 | 模型可查询与中断。 | 支持 per-session live 与全局 scheduler capacity 分层;也说明深度和并发不能互相替代。 |
| OpenClaw | 默认深度 1、每父 live children 5、并发池 8/4,另有消息循环上限。 | spawn 时再次检查深度、children 和并发。 | runId/session key;宿主维护 run;已确认子任务级联取消。 | list 和宿主状态。 | 采用“提交处再检查”和级联思路;不采用多个重叠 children/global 参数。 |
| OpenCode | 权限形状形成隐式深度 1;未确认数值并发上限。 | 工具权限在子 session 创建/调用处约束。 | 子 session id 和后台 metadata;宿主级联取消。 | 后台任务状态可见,但模型查询面较弱。 | 证明 permission 限制不等同于资源 quota;状态应当独立暴露。 |
| Hermes | 默认深度 1(可配至 3)、child concurrency 3、每子 50 iterations、caller timeout 600 秒并有 heartbeat。 | 线程池提交和调用等待处限制。 | 同步等待;模型没有稳定 job id;宿主可级联取消。 | 看板/宿主可见,模型控制面有限。 | 采用 runtime/idle 两类时间信号;不把 caller wait timeout 当作实际停止保证。 |
| CodeBuddy | 官方本机包文档记录 workflow 最多并发 16、每次 run 最多 1000 agents。 | workflow 编排器按 phase 和 run 约束。 | 同 session 内可 resume;agent teams 文档同时说明 team 不由 /resume 恢复。 | workflow UI 展示 phase agent 数、tokens、elapsed 和 abort。 | 采用并发、累计量和可见用量分离;不直接采用 16/1000,也不把 resume 描述扩大到 team。 |
| pi-mono | 示例扩展 batch 8、concurrency 4、单任务输出 50KB。 | 扩展在派生子进程前限制 batch/concurrency。 | 没有 durable job id/query;一层进程取消可能遗留更深后代。 | 主要是调用结果。 | 说明扩展局部 semaphore 不足以覆盖持久任务和级联恢复。 |
采用
- 全局 scheduler capacity 与 per-session live 分层。
- live、queued、累计任务分别计数。
- 稳定 job id、级联取消、用户可查询状态。
- tokens、elapsed、reason 在任务视图中同时展示。
修改或拒绝
- 拒绝直接采用竞品具体数值。
- 拒绝用深度、fanout 或 tool permission 代替资源 admission。
- 拒绝只用进程内 semaphore;admission 必须持久且跨进程原子。
- 拒绝把 caller timeout 或终态标记描述为已终止执行。
03 我们后续怎么计划:按用户使用流程建立一个治理边界
04 用户查看和配置全局默认与会话覆盖
- 当前行为
- 配置 schema 已包含协作限制与 agent.resource_limits;resolver 已解析 global/session/parent/job 来源,worker capacity 由环境变量控制。
- 用户影响
- 配置存在不等于所有执行入口和用户界面已验证;用户面应显示配置值、来源与有效值,避免把未接入的限制呈现为已强制。
- 缺口
- 若同时增加多组全局、父级、子级同义参数,优先级难以解释;若使用 0 同时表达 unlimited 与 disabled,会延续现有配置歧义。
- 目标行为
- 一个最小
agent.resource_limits对象承载全局默认;session metadata 可覆盖同名字段。API 始终返回 source、configured 和 effective 三者。任务只能在 effective session 限制内进一步收紧。
agent.resource_limits: max_live_per_session: integer | null max_queued_per_session: integer | null max_jobs_per_session: integer | null max_total_tokens: integer | null max_cost_usd: decimal-string | null max_runtime_seconds: integer | null idle_timeout_seconds: integer | null
所有非空值必须大于 0;null 表示未设置/继承。为兼容现有安装,默认不启用 queued、累计和四类预算。max_live_per_session 未设置时,有效值等于当前 scheduler capacity;已设置时,有效值是配置值与 scheduler capacity 的较小者。OPENPROGRAM_JOB_WORKERS 继续是唯一全局执行容量来源,并以只读字段 scheduler_capacity 展示。
只有 owner 可以修改全局默认和 session override。Agent/task 请求只能提供更严格的四类执行预算,不能修改 live、queued、累计任务量或提高任何 owner 限制。现有 config get/set 接口扩展同名字段,session override 复用 session metadata;不增加第二套 quota 配置服务。
max_total_tokens 和 max_cost_usd 统计该 session 中带受治理 job_id 的 agent/Job 调用总量;普通前台主对话不计入本设计。max_runtime_seconds 和 idle_timeout_seconds 是每个 Job 及其祖先 scope 的时间 ceiling。修改 live/queued/cumulative 上限后,不终止已经 live/queued 的任务,但立即限制新的 admission 或 queued → live;修改 token/cost 上限后,已有任务在下一次 reservation 使用新 session ceiling;runtime/idle 默认值只作用于修改后 admission 的 Job,避免给运行中任务追溯设置 deadline。
05 Agent 请求创建任务时的 admission 与原子预占
- 当前行为
- agent tool 的同步/异步及忙目标派活已调用 runner/governor admission;governor 以事务检查 depth、fanout、queue 和累计数量,模型入口还保留进程内 fanout 预检。普通消息的忙目标路径先进入 inbox,再在投递时创建 Job。
- 用户影响
- 相同请求从不同入口可能获得不同结果;并发前端或多进程可以同时通过非原子检查;失败创建可能留下 Job、branch、inbox 或计数残留。
- 缺口
- 统一 admission、跨进程事务与幂等身份已有源码;各入口拒绝时不留下 Job 创建副作用,以及普通消息排队后准入失败的通知行为仍需逐项验证。
- 目标行为
- 所有能产生执行任务的路径必须调用
ResourceGovernor.admit_job。先做纯校验,再在 SQLite 事务中原子检查 depth/fanout 与资源限制并预占,成功后才创建 Job、新分支及绑定 Job 的 inbox 条目,并发布执行已接纳事件。拒绝不创建 Job。独立完成消息授权的普通收件回执可以先于准入存在。
send_message 可以在 Job 创建前持久化消息回执,前提是通过消息授权和 inbox 容量限制。它与 agent(to=...) 创建的绑定 Job 的 inbox 条目不同。后续准入失败时,将该回执标记失败,并向发送方返回准入失败原因;不得声明执行已接纳。以下事务约束 Job 创建,不要求先前的普通收件也等待该事务。统一覆盖范围
- 同步与异步
agent();agent(to=...)的 busy-target 预创建;run_agent_turn_async。 - WebSocket/API spawn;模型工具;未来任何直接调用
JobRunner.spawn_job的入口。 - 同一
job_id和相同 request fingerprint 的重试是幂等读取,不能再次增加 fanout、累计量或预占 queue;内容不同则返回quota.admission_conflict。 send_message不创建 Agent 分支,但由它触发的新 Job 必须 admission,并计入运行目标 session 的累计任务数及资源预算。普通 inbox 收件本身尚未创建 Job,不应预报执行已获接纳。
跨 SQLite 与 jobs.json 的 durable intent
- 生成
job_id、admission_id与 request fingerprint,完成参数、target 和权限等不修改状态的校验。 - 在 usage DB 的第一个
BEGIN IMMEDIATE事务中检查 depth、per-turn fanout、session/parent 剩余量,插入preparingadmission,并预占一个 queue 名额、一次 provisional fanout 和一次 provisional 累计任务量,然后提交。preparing必须在写 jobs.json 前已经 durable;所有并发 admission 检查都把它的 provisional 占用计入,避免进程间超配。 - 使用
openprogram._compat.flock保护每会话 tasks 文件读改写,持久化 Job。此时不广播,也不创建其他不可逆副作用。 - 在第二个
BEGIN IMMEDIATE事务中将 admission 从preparing改为queued,把 provisional fanout/累计量确认为正式计数并提交;之后才创建分支及绑定 Job 的 inbox 条目,并广播执行已接纳状态。已有普通消息回执在投递时关联获准入的 Job。 - 任一步失败均执行幂等补偿。重启看到 durable
preparing时:存在匹配 Job 就以第二个事务 finalize;不存在就以事务释放 queue 与 provisional 计数。SQLite 事务不得跨越 jobs.json 文件 I/O。
{accepted:false, job_id:null, reason_code, retryable, effective_limits, usage, capacity}。queue 满通常可重试;累计量或父预算耗尽不可重试,除非 owner 提高限制或创建新会话。06 排队与运行中的实时并发
- 当前行为
- runner 使用持久 admission 与 dispatcher;governor 在领取 queued 工作前检查 session/global live 容量。排队任务尚无 live claim。
- 用户影响
- 已有队列/累计配置和 live claim,但完整跨入口限制、公平性及队列诊断仍需要验证证据。
- 缺口
- 源码已有 durable dispatcher 和原子资源 claim;公平性、worker 丢失恢复及跨进程准入/释放仍需完整验收。
- 目标行为
- admission 后先进入持久队列。dispatcher 只在全局 worker 和该 session live 都有容量时提交任务,并在同一事务中释放 queue、获取 live。排队不占 live。
Job 的兼容状态仍是 pending/queued/running/completed/cancelled/errored;资源状态单独持久化。stopping 尤其重要:Job 可以已显示 cancelled,但只要 worker/thread/runtime 尚未确认停止,live lease 就不能返还。这样不会因为 30 秒 watchdog 只改状态而产生超配。
dispatcher 按全局 admitted_seq 选择“当前 session 仍有 live 容量”的最早任务。它跳过暂时不可运行的 session,避免某个 session 的队首任务阻止其他 session 被调度。全局 worker pool 仍决定总执行容量。
只有持有现有 worker lock 的单个执行进程可以执行 queued → live 并提交 executor;Web/TUI/CLI/API 进程只做 admission 和持久入队。这样保留当前单 worker 产品拓扑,也防止多个进程各自创建全局线程池。若未来支持多个执行 worker,需要另行设计全局调度。
07 父子预算分配与只可收紧继承
- 当前行为
- Job 保存 budget_scope_id 与 effective_limits;governor 创建关联父级的 scope、解析祖先上限,并在 provider reservation 事务中检查祖先余额。provider 接入状态见 §01。
- 用户影响
- scope 模型支持共享父预算,而不是为每个子任务提供独立保证额度。用户需同时看到有效上限与 shared_remaining,区分局部上限和当前可用共享余额。
- 缺口
- 原语与生产 reservation 路径已经存在;显式子预算、兄弟竞争及全部入口的上下文传递仍需按完整矩阵验收。
- 目标行为
- task 请求可带单个
limits对象,只包含 token/cost/runtime/idle 四类预算。显式子上限不得大于 session 有效上限、父任务上限或父 scope 的当前剩余量。
未显式分配子预算时的确定规则
不提前把父预算平均切分,也不按 fanout 数推测未来任务。子任务加入父级共享 budget scope;每次 provider/tool reservation 都在一个事务中检查“任务自身上限、全部祖先 scope、session scope”,最先成功的 durable reservation 获得使用权。这样不会因未使用的静态份额导致预算闲置,也不会把“当前剩余”误报为子任务保证额度。
UI 将 shared_remaining 标为共享实时余额,不称为 allotment。显式子预算只创建更严格的局部 ceiling,不从父 scope 预先永久扣除;实际 reservation 才占用父余额。
08 token、费用、wall-clock 与 idle 的计量和强制停止
- 当前行为
- 受治理的 provider 调用通过 BudgetedRequest 在凭证或网络访问前预占、收紧 output cap,并结算终态 usage。runner 已有 budget loop 与 bounded-operation 检查;不受该治理的历史兼容路径仍使用 best-effort usage 记录。
- 用户影响
- 限制是否生效取决于 Job 归属及经过审核的 provider/操作边界。未知费用必须与零费用区分;返回超时结果本身不能证明同步线程已停止。
- 缺口
- 源码已有 reservation、归属、未知费用处理、活动记录及操作边界。adapter 覆盖、晚到结算与各操作类型的停止确认仍需运行验收,本轮没有重跑这些 gate。
- 目标行为
- 实际用量只由现有 usage ledger 记账;在同一 DB 增加 reservation 状态。模型请求和工具调用开始前原子预占,结束后以 provider 最终 usage 结算。主动 timer 与边界检查共同触发既有级联取消。
| 预算 | 计量单位与事实来源 | 调用前强制 | 调用中/调用后 | 不能可靠强制时 |
|---|---|---|---|---|
| token | provider-authoritative input + output;cache read/write 单独诊断,不重复相加。 | 预占 input_upper_bound + max_output_tokens,并把 provider output cap 收紧到剩余值。 | 最终 usage event 结算并释放差额;并发请求将 open reservations 一并计入。 | 严格 token budget 下若 tokenizer 或安全上界不可得,拒绝新模型请求:quota.accounting_unavailable。 |
| cost | 已知 provider/model/rate 与实际 token;cost_source 保留。 | 仅在所有需要的价格已知时预占 micro-USD 上界。 | 按实际 token/rate 结算;聚合同时返回 cost_known 和 unknown_cost_events。 | 设置 cost budget 时,当前调用价格未知或该 scope 已含未知费用事件,都拒绝新 LLM 调用:quota.cost_unavailable。未设置 cost budget 时可执行,但费用显示“未知”,绝不显示为 $0;不根据后来的目录价格改写历史实际费用。 |
| runtime | 从资源状态进入 live 起的 monotonic wall-clock;排队等待不计入。 | 每个 provider/tool timeout 取操作上限与 task 剩余时间的最小值。 | 主动 timer 到期设 cancel,并沿现有 parent-child 关系级联。 | 不能在剩余时间内可靠退出的工具不得进入严格预算任务,返回 error.nonpreemptible_operation。 |
| idle | 最后一次有意义活动的 monotonic 时间。 | 调用开始记录边界;等待子任务时关联其活动。 | provider 数据、tool progress、子任务 progress/terminal 更新活动时间;transport keepalive 不更新。 | 工具若既无进度也无有界 timeout,按不可抢占操作处理。 |
不可抢占操作的明确上限
预算耗尽在模型请求/工具调用边界强制,并由 active timer 尽快触发取消。正在执行的操作不会被描述为即时终止。其最大不可抢占区间必须等于“操作声明 timeout、任务剩余 runtime、系统 cancel grace”三者中的最小值。同步 in-process 操作如果无法满足该约束,需要改为可终止子进程或被严格预算任务拒绝;asyncio.wait_for 返回不代表 executor 线程已经结束。
记账延迟与崩溃
- reservation 状态为
reserved → started → settled,或在 provider I/O 开始前由reserved → released。凭据解析、请求配置或 provider generator 建立失败时释放尚未 started 的 reservation;start成功后发生取消、transport error 或缺失 terminal usage 时保留保守 exposure,等待同一 reservation 的晚到结算。不能用“尚未收到首个 stream event”推断请求未到 provider。 - 请求已经开始但拿不到最终 usage 时,保留 conservative unsettled exposure;不能捏造实际 token/cost,也不能立即返还并允许重复超支。
- 预算任务在调用前无法访问 accounting DB 时 fail closed。调用后 settlement 失败则任务 errored、停止后续调用,并保留 reservation 供恢复器核对。
- 当前 recorder 的 best-effort 行为可继续服务非预算历史兼容路径;进入预算治理的调用必须使用可确认的 reserve/settle 协议。
09 取消、失败、超时和完成后的名额释放与累计量语义
- 当前行为
- Job 终态与资源 admission/lease 状态已经分离。governor 保留累计 admission,支持 stopping 与确认释放;runner 完成及恢复路径使用这些原语。
- 用户影响
- 显示 cancelled 的 Job 在执行停止期间仍可能占用 live 名额。界面需区分取消请求、确认退出与配额释放。
- 缺口
- 状态分离和累计保留规则已有代码定义;watchdog、晚到完成及崩溃交错仍需完整矩阵验证。本轮没有该验证证据,不表示缺少这些原语。
- 目标行为
- queue/live 只在其执行实体确认结束或 lease 恢复规则成立时释放;累计任务量在成功 admission 时只增加一次,任何终态都不返还。
| 事件 | queue 名额 | live 名额 | 累计任务量 | Job 终态 |
|---|---|---|---|---|
| admission 被拒绝 | 不占用 | 不占用 | 不增加 | 不创建 Job |
| preparing 回滚 | 释放 | 不占用 | 不增加 | 没有 Job 或恢复为一致状态 |
| queued 后用户取消 | 立即释放 | 不占用 | 保留 | cancelled |
| running 后取消/预算耗尽 | 已释放 | 进入 stopping,确认退出后释放 | 保留 | cancelled |
| 执行错误/完成 | 已释放 | worker finally 确认后释放 | 保留 | errored/completed |
| 相同 job_id 重试 | 不重复占用 | 不重复占用 | 不重复增加 | 返回原任务/冲突原因 |
累计量的生命周期等于 session 生命周期。默认没有隐式 reset;session archive 也不返还。用户只能创建新 session,或由 owner 明确提高 session override。减少上限不能删除既有任务;当已用量高于新上限时,只拒绝后续 admission。
10 重启恢复、多进程竞争和诊断
- 当前行为
- Job 存储已有跨进程 flock。runner 启动及 reconcile 循环使用持久 admission、owner lease 与 ResourceSaga 恢复;未关联的旧 Job 继续按既有孤儿策略处理。worker lock 保持单 worker 执行。
- 用户影响
- admission 与恢复跨越 SQLite、Job 文件和运行所有权。诊断需区分恢复排队、worker 丢失和资源释放,并保留原执行身份。
- 缺口
- owner/lease、preparing reconciliation、文件锁和结构化原因原语已经存在。进程退出时机、并发前端写入及重复恢复仍需按矩阵验收,本轮未重跑。
- 目标行为
- 单 worker 仍是支持的执行拓扑;多个 UI/API 进程通过 SQLite admission 事务竞争。每个 live admission 带 owner/lease;reconciler 按 intent、Job 和 lease 三方事实恢复,不让配额永久泄漏。
恢复规则
preparing + Job 存在:校验 admission/job id 后 finalize 为 queued;preparing + Job 不存在:回滚 queue 与累计 provisional delta。queued在重启后继续进入 durable dispatcher,不需要占 live。live/stopping的 owner 已消失且 lease 过期:先按现有孤儿策略将 Job 标为 errored,reason 为error.worker_lost,再释放 live;未过期不由另一个实例抢占。初始实现固定每 10 秒续租、lease TTL 30 秒,使用 UTC wall-clock 持久化;只要原 owner 仍持有 worker lock,即使 wall-clock lease看似过期也不得释放。该参数不是用户 quota 配置。- lease 只证明 worker ownership,不是外部副作用已经回滚。Job 输出中保留 worker-lost 诊断。
- jobs.json 所有读改写加跨进程
flock;SQLite 使用 WAL、BEGIN IMMEDIATE和唯一键保证 admission/reservation 幂等。
本设计不引入集群 consensus 或远端 scheduler。若未来允许多个 worker 同时执行,同一 job_admissions lease/owner 契约可扩展,但需要独立设计 leader election、clock assumptions 和外部 runtime fencing。
11 Web、TUI 和 CLI 的用户可见状态与原因码
- 当前行为
- 已有 JobResourceView 与稳定资源原因码,以及 Web 订阅、终端/CLI 适配和 list_jobs/job_output 投影。取消使用 canonical execution 控制入口。
- 用户影响
- 各界面需一致解释排队、累计量、费用是否已知及 stopping。存在数据投影不等于每个状态的所有字段都已正确显示。
- 缺口
- 公共 DTO 已存在;跨界面字段/原因码一致性、旧记录、未知费用显示及恢复状态仍需运行和可视验收。这里不宣称已有独立 Tasks panel。
- 目标行为
- Web、TUI、CLI 和模型工具消费同一
JobResourceView;文字可以适配界面,字段和 reason code 必须一致。
| 视图 | 必须显示 | 交互 | 兼容方式 |
|---|---|---|---|
| Web | Job status + resource state、queue position、session live/queued/cumulative、四类剩余额、cost known/unknown、reason。 | 沿用现有 task/branch 卡片和 cancel;不新增后台会话 UI。 | 现有 job_status event 增加可选 resource 字段。 |
| TUI | 紧凑 session 容量、选中任务剩余额和停止原因。 | 复用任务列表/详情/停止操作;不要求新的全屏产品面。 | 字段缺失时按 legacy task 展示,不猜测 0。 |
| CLI | 人类可读表格与稳定 JSON:effective limits、usage、reservations、reason、retryable。 | 复用任务查询与取消命令面;JSON 供自动诊断。 | 新增字段向后兼容,Job 原状态枚举不变。 |
| 模型工具 | list_jobs、job_output 与 canonical execution 控制入口返回同一 reason 和简化剩余额;JobRunner.cancel_execution 是内部方法。 | admission 拒绝是结构化 tool result,不创建不存在的 Job 记录。 | 原文本结果保留,附加机器可读字段。 |
稳定 reason code
| 阶段 | reason_code | 语义 |
|---|---|---|
| admission | quota.queue_full | 当前 session 持久队列已满,可在容量释放后重试。 |
| admission | quota.jobs_exhausted | session 累计任务量已达到上限。 |
| admission | quota.parent_budget_exhausted | 父 scope 剩余量不足以接受子任务限制。 |
| admission | quota.token_exhausted / quota.cost_exhausted | 请求前已无足够 reservation 空间。 |
| admission | quota.cost_unavailable | 设置了 cost budget,但 provider/model 价格未知。 |
| admission | quota.invalid_limits | 值非法或子限制比父/会话限制更宽。 |
| admission | quota.accounting_unavailable / quota.admission_conflict | 无法安全记账,或幂等 key 与已有请求内容冲突。 |
| terminal | cancel.user / cancel.parent / cancel.session | 明确取消来源。 |
| terminal | budget.token_exhausted / budget.cost_exhausted / budget.runtime_exhausted / budget.idle_exhausted | 运行中达到预算边界,Job 进入 cancelled,取消向后代级联。 |
| terminal | error.worker_lost / error.accounting_unavailable / error.nonpreemptible_operation | 执行保障失败,Job 进入 errored。 |
| terminal | completed | 正常完成。 |
12 稳定 schema 与状态契约
实际 usage 继续以 usage_events 为唯一事实源。下列 admission/reservation 表只描述资源占用和未结算 exposure;不能用于伪造实际 provider token/cost。
job_admissions(
admission_id PRIMARY KEY,
job_id UNIQUE NOT NULL,
session_id NOT NULL,
parent_job_id,
caller_turn_id,
creates_agent BOOLEAN NOT NULL,
request_fingerprint NOT NULL,
budget_scope_id NOT NULL,
state CHECK state IN ('preparing','queued','live','stopping','released'),
admitted_seq NOT NULL,
owner_instance_id,
lease_expires_at,
created_at, started_at, last_activity_at, released_at,
reason_code
)
budget_scopes(
budget_scope_id PRIMARY KEY,
scope_kind CHECK scope_kind IN ('session','task'),
session_id NOT NULL,
job_id UNIQUE,
parent_scope_id,
max_total_tokens,
max_cost_microusd,
max_runtime_seconds,
idle_timeout_seconds,
created_at
)
usage_reservations(
reservation_id PRIMARY KEY,
job_id NOT NULL,
budget_scope_id NOT NULL,
kind CHECK kind IN ('token','cost'),
state CHECK state IN ('reserved','started','settled','released'),
reserved_tokens,
reserved_cost_microusd,
request_started_at,
settled_event_id,
expires_at
)
usage_events + optional columns:
job_id, budget_scope_id, reservation_id
Job JSON + optional fields:
admission_id, budget_scope_id, effective_limits, reason_code
effective_limits 是任务开始时的审计快照;配置查询另行返回每个字段的 configured、effective 与 source。实际可用共享余额必须从 ledger + open reservations 实时计算,shared_remaining 明确表示祖先和 session 共享池中的当前值,不是该 Job 的保证额度。Job JSON 不复制累计 actual usage。金额配置使用十进制定点字符串,存储和比较使用 micro-USD 整数,避免浮点边界差异。
JobResourceView
{
"job_id": "...",
"status": "running",
"resource_state": "live",
"reason_code": null,
"retryable": false,
"capacity": {
"scheduler_capacity": 4,
"session_live": {"used": 2, "limit": 3},
"session_queued": {"used": 1, "limit": 8},
"session_jobs": {"used": 17, "limit": 100},
"queue_position": null
},
"budget": {
"scope": "task_with_shared_ancestors",
"tokens": {"actual": 12000, "reserved": 4000, "limit": 50000},
"cost_usd": {"actual": "0.38", "reserved": "0.12", "limit": "2.00",
"known": true, "unknown_events": 0},
"runtime_seconds": {"used": 91, "limit": 600},
"idle_seconds": {"used": 4, "limit": 120},
"shared_remaining": {"tokens": 34000, "cost_usd": "1.50"}
}
}
13 兼容、迁移和实施批次
兼容原则
- 旧 Job 缺少新增字段时标为
legacy/unmetered,不能把缺失解释为 0,也不重写历史 jobs.json。 - 现有 Job status 枚举、job id、状态广播和级联取消 API 保持兼容;资源状态与 reason 是可选扩展。
- 现有 depth/messages/fanout 配置和语义保持不变。它们是拓扑/循环 guard,不参与 quota 计数。
- 现有
OPENPROGRAM_JOB_WORKERS保持唯一全局 worker capacity。没有新增max_live_global。 - 旧库迁移只增加 nullable columns/tables/index;usage event 保持 append-only。
| 批次 | 实现范围 | 启用条件 | 源码状态;本轮未重跑验收 |
|---|---|---|---|
| 1 计量基础 | schema migration、task/budget attribution、unknown cost 聚合、只读 effective limits/diagnostics。 | 旧库升级/回退、未知费用和现有 usage 查询兼容测试通过。 | 已有迁移与 DTO 源码;本轮未重跑兼容性 gate |
| 2 Admission 与队列 | 统一创建入口、durable intent、per-session live/queued/cumulative、dispatcher、跨进程文件锁。 | 所有入口覆盖;并发进程不超配;每种终态和崩溃点可恢复。 | 已有 admission、dispatcher 与恢复源码;本轮未重跑全部入口及并发 gate |
| 3 Token 与费用 | 共享父子 scope、reserve/settle、provider output cap、unknown price fail-closed。 | 并发请求、晚到 usage、settlement failure 和价格变更测试通过。 | 已接入公共 provider 调用路径;本轮未重跑覆盖及故障注入 gate |
| 4 Runtime 与 idle | active timer、activity contract、deadline clamp、bounded operation contract、级联停止。 | 模型、工具、同步线程/子进程、stopping lease 测试通过。 | 已有 runtime/idle 循环与 bounded-operation 检查;本轮未重跑各操作类型 gate |
| 5 用户面与 gate | Web/TUI/CLI/model tool 同一 DTO、reason 文案、兼容文档和机械检查。 | 跨界面合同测试和本页全部 gate 通过。 | 已有公共资源 DTO 与界面适配;本轮未重跑跨界面 gate |
14 测试矩阵
| 测试域 | 最低用例 | 失败表示什么 |
|---|---|---|
| 配置 | 全局默认、session override、parent/task 只收紧;null/正整数/decimal 校验;worker env 只读展示。 | 有效值不可预测或出现重叠全局上限。 |
| 协作与资源限制交互 | 代数用完仍可在消息/admission 限制内投递已有分支;消息用完拒绝新投递,但保留有权执行的查询/取消;send_message 创建的 Job 计入目标 session admission;协作 0 与资源 null 语义不同。 | 可绕过资源限制,或已有执行无法查询/停止。 |
| 创建入口 | sync/async agent、target inbox、Web/API、runner 内部、未来注册入口;被拒绝的 Job 创建不留下新 Job、branch 或绑定 Job 的 inbox 条目;先前收到的普通消息若后续准入失败,其回执须标记失败并通知发送方。 | 存在 quota bypass。 |
| 原子 admission | 20+ threads/processes 同时创建;唯一 job id;上限边界;失败注入覆盖 preparing 前后。 | 超配、重复累计或永久泄漏。 |
| Queue/live | queue 不占 live;queued→live 原子交换;跨 session oldest-eligible;queue full;stopping 保留 live。 | 会话长期无法调度、名额错误提前释放或 executor 无界排队。 |
| 累计量 | completed/cancelled/errored 均不返还;preparing rollback 不计;同 job id 重试不重复;降低上限。 | 累计语义随终态变化或可被重试绕过。 |
| 父子预算 | 显式 child 超父剩余被拒;无显式预算共享 scope;兄弟并发 reservation;多层祖先检查。 | 子任务可扩大预算或并发超支。 |
| Token/cost | 安全 token 上界、provider output cap、cache token 不重复;known/unknown price;late/missing usage;settlement DB failure。 | 预算只能事后观测,或未知费用被当 0。 |
| Runtime/idle | queue wait 排除;provider data/tool progress/child activity;keepalive 不重置;deadline clamp;不可抢占工具拒绝。 | 时间预算不可强制或会误杀活跃任务。 |
| 取消与恢复 | 用户/父/session/budget 级联;watchdog 后线程仍活;worker 在每个事务点崩溃;lease expiry;reconciler 幂等。 | 名额提前释放、后代残留或重启后永久占用。 |
| 用户可见性 | Web/TUI/CLI/模型 DTO 合同;排队位置、共享剩余额度、未知成本、reason/retryable;旧任务。 | 各界面解释不同或把未知显示为 0。 |
15 Feature matrix 实心圆机械 gate
feature matrix 的“并发数与每会话派生总量上限”在基础基线 当前工作树基线 尚未满足下列机械 gate;当前工作树是否满足这些条件必须由对应源码和验证结果逐项确认,不能用候选分支记录代替:
- 稳定配置 schema 已实现,并能从 Web/TUI/CLI 查看全局默认、session override 与有效值。
- 所有 agent/Job 创建入口都经过统一 admission;不存在 Web、同步 agent、target inbox 或 runner 直调绕过。
- per-session live 与 cumulative task admission 在跨进程竞争下原子,不超配、不重复计数。
- queue 是否占 live、累计量是否返还、stopping 是否保留 live 均按本页语义实现。
- 完成、失败、取消、超时、worker crash 和 admission 中间崩溃均能释放应返还的 queue/live,不永久泄漏。
- Web/TUI/CLI 展示有效限制、已用量、queue/live/cumulative 和稳定 reason code。
- 兼容迁移、并发/崩溃测试、文档链接和 feature matrix 机械计分检查全部通过。
16 当前实现与验证边界
本节记录当前工作树中可直接定位的实现和仍需验收的边界。它不记录日期、commit SHA 或迁移过程;历史演变通过 git 查看。
| 范围 | 当前源码证据 | 当前状态 | 仍需确认的缺口 |
|---|---|---|---|
| Job 实体与状态 | openprogram/agent/job/types.py:Job、JobStatus、合法转移和 worktree_id;openprogram/agent/job/store.py:持久化读写与状态更新。 | 已实现 | 崩溃恢复、终态幂等和跨进程竞争仍需按完整矩阵验收。 |
| Runner 与 admission | openprogram/agent/job/runner/:JobRunner、durable dispatcher、reconciler、budget loop、spawn_job、cancel_execution、list_jobs、await_job;ResourceGovernor 在 runner 初始化和运行链路中使用。 | 已实现并接入主要执行链 | whole-product gate、跨进程竞争和 worker-loss 证据本轮未重跑。 |
| Job WebSocket 与模型工具 | apps/server/openprogram_server/_webui/ws_actions/job.py:spawn/list/get;openprogram/programs/tools/agents/agent/list_jobs/、job_output/;runner 提供取消。 | 主要入口已实现 | 取消入口一致性和 DTO/reason/retryable 回归本轮未重跑;独立 Tasks panel 不属于当前实现。 |
| Web 状态 | apps/web/lib/net/use-ws.ts:job_status、spawn_job_result;apps/web/components/right-sidebar/branches/index.tsx 与 attach-card.tsx:状态和资源订阅。 | 主要用户面已实现 | 独立 Tasks panel 不属于当前实现;资源剩余、queue position、unknown cost 和恢复状态的回归测试本轮未重跑。 |
| ResourceGovernor 准入、租约与协调 | openprogram/agent/resource_governance/:admit_job、claim_next、renew_lease、reconcile、scope 和 resource view;runner 已接入。 | 原语与主要执行接线已实现 | token/cost provider chokepoint 仍需逐 adapter 确认;本轮未重跑全量验证。 |
| 验证边界 | 当前文档和源码抽查提供路径与符号依据;历史验证记录保留在 Git。 | Not reverified | 文档构建和检查与产品执行验收分别记录。本轮未重跑产品运行 gate;不能因此判定缺少实现。 |
状态规则。“原语存在”“入口能返回状态”和“完整执行链已通过验收”是三个不同状态。后续更新本页时必须分别填写,不能用历史审查表或候选分支记录代替当前工作树证据。
证据索引
| 主题 | 本仓库证据 |
|---|---|
| JobRunner、单 worker、取消和孤儿恢复 | openprogram/agent/job/runner/;openprogram/agent/job/store.py;openprogram/agent/job/types.py;openprogram/worker/paths.py |
| 派生和消息预算 | openprogram/programs/tools/agents/agent/agent/agent.py;openprogram/programs/tools/agents/send_message/send_message/depth.py;openprogram/config_schema.py |
| Usage 与 provider 记录 | openprogram/usage/event.py;openprogram/usage/ledger.py;openprogram/usage/recorder.py;openprogram/providers/stream.py |
| Deadline 与函数 timeout | openprogram/providers/utils/deadline.py;openprogram/programs/_runtime.py |
| 任务界面与协议 | apps/server/openprogram_server/_webui/ws_actions/job.py、Web branch/task cards、openprogram/programs/tools/agents/agent/list_jobs/、job_output/、JobRunner.cancel_execution |
| 竞品与既有设计 | agent-collab-comparison.html;Agent 协作设计;async-job-lifecycle.zh.html;usage-metering.zh.html |