Agent 运行配额与任务生命周期治理

按用户从配置、创建、排队、运行到终态诊断的完整流程,说明 OpenProgram 当前已有什么、参考实现怎样限制资源,以及后续需要建立的统一治理契约。

文档状态设计与实现部分完成,当前链路仍有缺口
产品边界单用户、本机、常驻 worker
验收状态原语与部分入口已实现,完整验收未完成
实现状态边界。当前 Job、ResourceGovernor、admission、资源投影与时间边界已经存在,但这不等于所有 provider、runtime/idle、Web/TUI/CLI/model 入口和恢复路径都已完成统一接入。下文只把源码和当前可见协议列为证据,并单独列出尚未完成的执行链和用户面缺口。

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_capacitysession / 全局 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 中,以下只区分源码确认的实现、源码确认的缺口和本轮未重新验证的范围。

现有机制已经确认的行为治理缺口证据
全局执行池与 admissionOPENPROGRAM_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 / costBudgetedRequest 已将公共 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/
创建入口审计。基础批次已把 runner 与同步 agent 接到 governor,但完整 gate 仍要求 WebSocket、busy-target inbox、内部重试和后续新增入口都有同一 admission 证据,并验证被拒绝的 Job 创建不留下新 Job、branch、绑定 Job 的 inbox 条目或执行已接纳广播。普通消息回执按 §05 的独立规则处理。

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 我们后续怎么计划:按用户使用流程建立一个治理边界

01查看与配置全局默认、会话覆盖、有效值
02请求创建统一 admission、原子预占
03排队与运行queued 与 live 分离
04父子预算只收紧、共享剩余池
05计量与停止token、成本、运行时长、空闲时长
06终态释放名额返还、累计量保留
07重启恢复intent、lease、多进程竞争
08用户诊断Web/TUI/CLI 同一 DTO

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 创建,不要求先前的普通收件也等待该事务。

统一覆盖范围

跨 SQLite 与 jobs.json 的 durable intent

  1. 生成 job_id、admission_id 与 request fingerprint,完成参数、target 和权限等不修改状态的校验。
  2. 在 usage DB 的第一个 BEGIN IMMEDIATE 事务中检查 depth、per-turn fanout、session/parent 剩余量,插入 preparing admission,并预占一个 queue 名额、一次 provisional fanout 和一次 provisional 累计任务量,然后提交。preparing 必须在写 jobs.json 前已经 durable;所有并发 admission 检查都把它的 provisional 占用计入,避免进程间超配。
  3. 使用 openprogram._compat.flock 保护每会话 tasks 文件读改写,持久化 Job。此时不广播,也不创建其他不可逆副作用。
  4. 在第二个 BEGIN IMMEDIATE 事务中将 admission 从 preparing 改为 queued,把 provisional fanout/累计量确认为正式计数并提交;之后才创建分支及绑定 Job 的 inbox 条目,并广播执行已接纳状态。已有普通消息回执在投递时关联获准入的 Job。
  5. 任一步失败均执行幂等补偿。重启看到 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。
preparing→ queued→ 原子交换 live→ cancel/timeout stopping→ 已确认退出 released

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 与边界检查共同触发既有级联取消。
预算计量单位与事实来源调用前强制调用中/调用后不能可靠强制时
tokenprovider-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 线程已经结束。

记账延迟与崩溃

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 三方事实恢复,不让配额永久泄漏。

恢复规则

本设计不引入集群 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 必须一致。
视图必须显示交互兼容方式
WebJob 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语义
admissionquota.queue_full当前 session 持久队列已满,可在容量释放后重试。
admissionquota.jobs_exhaustedsession 累计任务量已达到上限。
admissionquota.parent_budget_exhausted父 scope 剩余量不足以接受子任务限制。
admissionquota.token_exhausted / quota.cost_exhausted请求前已无足够 reservation 空间。
admissionquota.cost_unavailable设置了 cost budget,但 provider/model 价格未知。
admissionquota.invalid_limits值非法或子限制比父/会话限制更宽。
admissionquota.accounting_unavailable / quota.admission_conflict无法安全记账,或幂等 key 与已有请求内容冲突。
terminalcancel.user / cancel.parent / cancel.session明确取消来源。
terminalbudget.token_exhausted / budget.cost_exhausted / budget.runtime_exhausted / budget.idle_exhausted运行中达到预算边界,Job 进入 cancelled,取消向后代级联。
terminalerror.worker_lost / error.accounting_unavailable / error.nonpreemptible_operation执行保障失败,Job 进入 errored。
terminalcompleted正常完成。

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 兼容、迁移和实施批次

兼容原则

批次实现范围启用条件源码状态;本轮未重跑验收
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 与 idleactive timer、activity contract、deadline clamp、bounded operation contract、级联停止。模型、工具、同步线程/子进程、stopping lease 测试通过。已有 runtime/idle 循环与 bounded-operation 检查;本轮未重跑各操作类型 gate
5 用户面与 gateWeb/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。
原子 admission20+ threads/processes 同时创建;唯一 job id;上限边界;失败注入覆盖 preparing 前后。超配、重复累计或永久泄漏。
Queue/livequeue 不占 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/idlequeue 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;当前工作树是否满足这些条件必须由对应源码和验证结果逐项确认,不能用候选分支记录代替:

  1. 稳定配置 schema 已实现,并能从 Web/TUI/CLI 查看全局默认、session override 与有效值。
  2. 所有 agent/Job 创建入口都经过统一 admission;不存在 Web、同步 agent、target inbox 或 runner 直调绕过。
  3. per-session live 与 cumulative task admission 在跨进程竞争下原子,不超配、不重复计数。
  4. queue 是否占 live、累计量是否返还、stopping 是否保留 live 均按本页语义实现。
  5. 完成、失败、取消、超时、worker crash 和 admission 中间崩溃均能释放应返还的 queue/live,不永久泄漏。
  6. Web/TUI/CLI 展示有效限制、已用量、queue/live/cumulative 和稳定 reason code。
  7. 兼容迁移、并发/崩溃测试、文档链接和 feature matrix 机械计分检查全部通过。
计分边界。该矩阵行只验收“并发数与每会话派生总量上限”,不要求 token/cost/runtime/idle 四类预算全部完成才改这一行;四类预算若以后形成独立能力项,应使用各自完整 gate。反过来,仅完成 UI、schema 或单进程 semaphore 也不能把本行改为实心圆。

16 当前实现与验证边界

本节记录当前工作树中可直接定位的实现和仍需验收的边界。它不记录日期、commit SHA 或迁移过程;历史演变通过 git 查看。

范围当前源码证据当前状态仍需确认的缺口
Job 实体与状态openprogram/agent/job/types.py:Job、JobStatus、合法转移和 worktree_id;openprogram/agent/job/store.py:持久化读写与状态更新。已实现崩溃恢复、终态幂等和跨进程竞争仍需按完整矩阵验收。
Runner 与 admissionopenprogram/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 与函数 timeoutopenprogram/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