深入学Agent Harness工程12Task System让目标跨轮持久化本篇对应的官方文档Learn Claude Codes12 Task System支撑 Task 数据结构、文件持久化、依赖检查、认领与完成流程。Python Data Classes用于核对dataclass、field(default_factory...)与asdict()的对象行为。OpenAI Agents SDK Context Management用于区分本地代码状态与实际进入模型输入的 LLM context。OpenAI Agents SDK Sessions用于区分会话历史持久化与独立业务任务状态。本篇主要内容第 11 篇用RecoveryState让一次 Agent Loop 能从截断、上下文过长和暂时限流中恢复但恢复状态只服务当前运行任务目标仍依赖对话历史。本篇新增Task数据结构和.tasks文件目录用blockedBy表达依赖用pending → in_progress → completed表达生命周期再把创建、查询、认领和完成注册成工具最后验证依赖解锁并说明循环依赖、并发覆盖、owner 失效等生产缺口。下篇预告Task 已经能够跨轮保存但慢命令仍会占住 Agent Loop。第 13 篇将把长操作移入后台并把完成通知重新注入前台消息。一、一次运行能够恢复为什么任务目标仍会随对话结束第 11 篇把错误分成输出截断、输入过长、暂时限流和不可恢复错误由RecoveryState控制 token 上调、续写、响应式压缩、退避与 fallback。它解决的是“当前模型调用怎样继续”并没有回答“跨越多个轮次、多个进程的目标保存在哪里”。假设任务是先生成接口定义接口定义完成后再实现客户端最后运行集成检查。只把这三步写在messages里会产生三个问题。第一响应式压缩可能删除早期目标。第二终端关闭后内存历史消失。第三模型每次都要从自然语言重新判断哪一步完成、哪一步阻塞状态没有可靠事实源。Memory 可以保存长期偏好和经验但不适合承担每个任务的实时状态机。第 12 篇因此把“对话”和“任务”拆成两条生命周期不同的数据messages保存模型本轮需要理解的交流、工具调用和结果Task 文件保存目标、依赖、owner 与状态模型需要任务事实时通过工具读取Harness 修改任务时写回独立存储而不是改写聊天文本。两条生命周期的差异要从“终点”观察上方 messages 可能因压缩或会话结束不再完整下方 Task 状态经过 JSON 写入后能够在进程重启时重新加载。它们可以相互引用但不能互相充当唯一事实源。OpenAI Agents SDK 的 Context Management 文档区分本地代码可见 context 与模型可见 contextSessions 文档则说明 session 会在多次 run 之间保存 conversation history。两者都能帮助理解边界持久会话保存的是交流历史Task System 保存的是业务目标状态。即使二者最终都落在数据库中也不应该用一张表或同一段文本混成一个概念。s12_task_system.py为了集中讲任务系统保留了基础 Agent Loop、Prompt 组装、文件工具和 Tool Calling却明确省略第 11 篇完整的RecoveryState、退避、上调和压缩代码。这不是说 Task System 替代 Error Recovery而是教学快照只展开本篇增量。真实 Harness 中两层应组合恢复层包住模型或外部调用任务层独立维护目标状态。左侧恢复层与右侧任务层都接在 Agent Loop 上却维护不同对象。恢复层回答“本次调用怎样继续”任务层回答“目标当前进行到哪里”省略一段展开代码不等于删除该层的架构职责。因此阅读第 12 份源码时不应得出“恢复能力消失”的结论。代码快照为了突出增量而简化组合架构上的两个模块仍可通过同一个 Agent Loop 和 handler 边界共同工作。二、持久任务图由哪些对象组成Task 的最小对象有六个字段。源码用dataclass声明id: str、subject: str、description: str、status: str、owner: str | None与blockedBy: list[str]让字段定义、初始化参数和序列化对象保持对应。这里先看字段之间的职责分区再进入文件读写代码。id提供稳定引用subject是短目标description保存补充说明status表示生命周期owner表示当前责任主体blockedBy保存必须先完成的任务 ID。对象图把身份与引用放在蓝色区域把状态和目标放在绿色区域把 owner 与依赖分别作为责任和图关系观察点。dataclass根据类型标注生成初始化和表示等方法代码再用asdict(task)转成可序列化字典。六个字段中只有description主要服务自然语言其余字段都参与程序判断或定位。模型可以提出 subject 与 descriptionID、状态转移、owner 身份和依赖合法性则应由 Harness 生成或校验不能全部接受模型自由填写。这里有两个细节。blockedBy是可变列表示例在create_task()中用blockedBy or []为每个 Task 创建新列表没有把同一个默认列表共享给所有实例。若直接写blockedBy: list[str] []dataclass 会拒绝这种可变默认值标准写法是field(default_factorylist)。第二status只是普通字符串。注释约定它只能是pending、in_progress、completed但类型系统和加载函数没有校验。任何 JSON 都可以写入status: runninggg。教学代码通过少量分支维持约定生产实现应使用 Enum、Literal 或验证模型并对存量文件执行 schema 校验。持久化层把每个 Task 写成.tasks/{task_id}.jsonTASKS_DIRWORKDIR/.tasksTASKS_DIR.mkdir(exist_okTrue)def_task_path(task_id:str)-Path:根据任务 ID 返回对应 JSON 文件路径。returnTASKS_DIR/f{task_id}.jsondefsave_task(task:Task):把任务当前状态写入独立 JSON 文件。_task_path(task.id).write_text(json.dumps(asdict(task),indent2))defload_task(task_id:str)-Task:从 JSON 文件恢复 Task 对象。datajson.loads(_task_path(task_id).read_text())returnTask(**data)文件使 Task 脱离 Python 进程退出程序后再次启动只要工作目录与.tasks保留list_tasks()仍能发现它们。它也便于观察教学状态每次认领或完成都会直接反映到 JSON。这条链的输入和输出都是 Task 对象中间 JSON 只是当前教学存储格式。只要 repository 接口保持save/load/list语义未来换成数据库不会改变上层状态机反过来若业务代码到处直接拼文件路径存储替换就会扩散到所有 handler。但“写到磁盘”只代表持久化不代表可靠事务。write_text()直接覆盖目标文件进程在写入中途崩溃可能留下不完整 JSON两个 Agent 同时修改同一 Task 会发生后写覆盖前写。生产实现至少要临时文件写入后原子替换、文件锁或数据库事务、版本号或 compare-and-swap并为损坏记录提供隔离与恢复。多个 Task 通过blockedBy形成有向依赖图。若 B 的blockedBy[A]B 只有在 A 的状态是completed时才可认领。can_start()就是这条边的运行判断defcan_start(task_id:str)-bool:确认所有前置任务都存在且已经完成。taskload_task(task_id)fordependency_idintask.blockedBy:ifnot_task_path(dependency_id).exists():returnFalseifload_task(dependency_id).status!completed:returnFalsereturnTrue缺失依赖被视为阻塞而不是自动忽略这能避免 B 在前置事实未知时提前开始。Task Graph 的关键不是画出很多方框而是让“某条边的前置状态是否满足”成为可重复计算的规则。依赖边只传递“是否允许开始”不会自动执行下游。A 完成后 B 从 blocked 变成可认领仍要经过claim_task()才进入in_progressB 完成后 C 才获得相同机会。这种逐边解锁避免模型仅凭一句“前置应该完成了”跳过事实检查。当前代码没有检查环。如果 A 阻塞于 BB 又阻塞于 A二者永远不能开始如果 Task 把自己放进blockedBy结果相同。创建任务时应该验证依赖存在、禁止自环并对整个图执行 cycle detection。缺失依赖、循环依赖和正常等待都表现为can_startFalse若不返回具体原因排障会非常困难。三、任务怎样创建、解锁并重新进入Agent Loop状态机只有三步pending已经创建尚未认领→in_progress依赖完成某个 owner 已认领→completed工作完成下游可能解锁claim_task()同时检查当前状态和依赖然后写入 ownercomplete_task()只允许从in_progress进入completed保存后扫描其他 pending Task报告刚刚解锁的下游defclaim_task(task_id:str,owner:stragent)-str:认领可启动任务并记录 owner。taskload_task(task_id)iftask.status!pending:returnfTask{task_id}is{task.status}, cannot claimifnotcan_start(task_id):returnfTask{task_id}is blockedtask.ownerowner task.statusin_progresssave_task(task)returnfClaimed{task.id}defcomplete_task(task_id:str)-str:完成任务并计算被解锁的下游任务。taskload_task(task_id)iftask.status!in_progress:returnfTask{task_id}is{task.status}, cannot completetask.statuscompletedsave_task(task)unblocked[item.subjectforiteminlist_tasks()ifitem.statuspendinganditem.blockedByandcan_start(item.id)]returnfCompleted{task.id}; Unblocked:{unblocked}这段代码真正改变了三个事实认领时 status 与 owner 一起写入完成时只允许从in_progress转移完成后重新计算下游can_start()。状态判断发生在 handler 内模型即使要求“直接完成 pending Task”也只会得到拒绝文本。状态机需要重点观察blockedBy分支与 owner 标签。依赖未完成时Task 仍处于 pending只是暂时不可认领owner 只在认领成功后出现并随 Task 一起持久化。owner 在这里提供了“谁正在处理”的可观察字段但还不是所有权约束。complete_task()没有接收当前 Agent 身份也没有验证完成者是否等于task.owner任何能调用工具的 Agent 都可以完成别人的任务。后续多 Agent 章节会继续扩展 owner而生产系统现在就应该把调用者身份交给 handler在服务端校验不能只依赖模型自觉传正确名字。为了让模型使用 Task System代码把五个本地 handler 注册为工具create_task、list_tasks、get_task、claim_task、complete_task。OpenAI-compatible Chat Completions 只承载函数名和 JSON 参数任务对象、JSON 文件、依赖检查与状态变更都留在 Harness。TOOLS[{name:create_task,description:Create a task with blockedBy dependencies.,parameters:{type:object,properties:{subject:{type:string},description:{type:string},blockedBy:{type:array,items:{type:string},},},required:[subject],},},# list_tasks / get_task / claim_task / complete_task 同样注册]TOOL_HANDLERS{create_task:run_create_task,list_tasks:run_list_tasks,get_task:run_get_task,claim_task:run_claim_task,complete_task:run_complete_task,}这段注册表只把稳定工具名映射到本地 handler没有把文件路径或 Task 对象直接暴露给模型。模型输出claim_task(task_id)后Harness 才解析参数、查找 handler、读取 JSON、验证状态并形成 tool result。协议交接需要沿箭头检查两种数据向右传的是函数名与 JSON 参数向左回到 messages 的是执行结果真正的状态写入始终停留在 Harness 一侧。因此Task System 不是 Chat Completions 提供的远程任务服务。兼容协议只让模型表达结构化意图.tasks的目录位置、并发策略、权限和生命周期都是应用自己的设计。Task 状态不会因为存在于.tasks就自动进入模型上下文。模型只有调用list_tasks或get_task并收到roletool结果后才看见它。也可以由 Harness 在每轮调用前选择少量相关任务注入 system但必须控制数量、权限和新鲜度。把整个任务库无条件塞进 Prompt 会重演第 08 篇的上下文膨胀问题。不调用模型端点也能对依赖解锁做本地状态推演步骤操作A 状态B 状态B 能否认领1创建 Apending不存在—2创建 BblockedBy[A]pendingpending否3认领 Ain_progresspending否4完成 Acompletedpending是5认领 Bcompletedin_progress已认领这项推演验证的是can_start()、claim_task()与complete_task()的本地控制流。它不验证模型是否总能规划出正确依赖也不验证并发进程下文件是否安全。模型负责提出任务操作Harness 必须独立校验状态转移。前后快照需要观察 B 的两次字段变化执行complete_task(A)后B 仍是 pending但can_start从假变为真执行claim_task(B)后B 才进入in_progress。把二者合并会让调度与执行责任混在一起。这一分离也为后续后台任务留下空间scheduler 可以寻找can_startTrue的 pending Taskworker 再以自己的身份认领没有 worker 时任务保持可认领而不会被误记为执行中。解锁事件还可以成为审计记录或通知但它本身不应该替 worker 完成认领。保留这个中间状态系统才能在多个可用 worker 之间执行权限、容量和优先级判断。四、文件任务图为什么还不是生产调度系统文件版 Task System 已经具备数据、依赖、状态、owner、持久化和工具入口却仍不是队列、工作流引擎或调度器。它不会主动选择任务不会启动 worker不会在未来时间唤醒也不会在 Agent 崩溃后重新分配in_progress任务。第 13 篇和第 14 篇才会分别增加后台运行与时间触发。当前实现至少存在八类工程缺口。ID 可能碰撞。task_{秒级时间戳}_{0000..9999}在高并发下不能保证唯一碰撞会覆盖已有文件。应使用 UUID、数据库序列或带唯一约束的 ID。写入不是原子的。write_text()可能留下半截 JSON并发写会丢更新。应使用临时文件与原子替换或交给带事务和锁的存储。没有版本控制。两个 owner 先后读取相同状态并写回时系统无法发现陈旧写入。可以增加version字段并做乐观锁。依赖没有图校验。自环、循环依赖、指向已删除任务的边都只表现为永远阻塞。创建与更新时需要验证 DAG并返回可诊断路径。owner 只是标签。认领没有租约完成不核对调用者Agent 崩溃后任务永远停在in_progress。生产状态通常还需要 lease、heartbeat、超时与重新认领。状态种类不足。任务只能完成不能失败、取消、暂停或等待人工输入。实际系统至少需要失败原因、重试次数、下次运行时间与终止原因。坏文件会拖垮列表。list_tasks()对每个 JSON 直接反序列化一个损坏文件就可能让整个列表失败。需要逐条隔离、schema 校验、错误目录和修复工具。权限边界缺失。Task ID 能直接映射文件名虽然示例 ID 由程序生成handler 仍应校验格式、workspace 与调用者权限避免任意路径、越权查询或跨项目修改。六条风险说明单文件方案的主要问题不是“性能不够快”而是缺少一致性和可恢复性。唯一 ID 解决引用碰撞原子写与事务解决中途失败版本锁解决陈旧覆盖DAG 校验解决永久阻塞身份校验约束 owner逐条隔离避免一份坏数据拖垮全局查询。从教学实现走向生产系统可以把职责拆成四层Task repository 负责原子读写和版本Task service 负责状态转移、依赖与权限scheduler 负责选择何时可运行worker 负责执行并汇报心跳。Tool handler 只调用 service不直接操作文件。这样未来换成 SQLite、PostgreSQL 或队列时Agent Loop 和 Tool Schema 不必重写。repository 的接口还应明确失败语义找不到 Task、版本冲突、依赖未完成、租约过期和记录损坏必须返回不同错误。若它们都变成Error: cannot complete上层恢复机制就无法判断应该重试、刷新状态、重新认领还是转人工。Task 也不应该完全相信模型参数。blockedBy中的 ID 必须存在且在同一作用域owner应来自认证上下文而不是任意字符串complete应携带预期版本或租约任务描述可能包含敏感信息返回给模型前要做权限与最小披露。Prompt 可以提示规则真正的拒绝必须发生在 handler 或服务层。第 12 篇的完整增量是用Taskdataclass 固定目标对象用.tasksJSON 让状态跨进程保存用blockedBy计算依赖用pending → in_progress → completed表达最小生命周期再通过五个工具把模型意图接到 Harness 的任务服务。messages仍负责交流Task 文件负责业务事实两者按需交接而不是互相替代。这套结构已经能回答“任务是什么、由谁处理、为什么被阻塞、完成后解锁了谁”却不能让慢任务自动在后台运行。若bash执行十分钟当前agent_loop()仍会同步等待期间无法接收新输入也无法可靠处理中断。第 13 篇将把长操作移出前台循环增加后台进程与完成通知再把结果作为新 observation 注入 Agent。
深入学Agent Harness工程(12):Task System让目标跨轮持久化
深入学Agent Harness工程12Task System让目标跨轮持久化本篇对应的官方文档Learn Claude Codes12 Task System支撑 Task 数据结构、文件持久化、依赖检查、认领与完成流程。Python Data Classes用于核对dataclass、field(default_factory...)与asdict()的对象行为。OpenAI Agents SDK Context Management用于区分本地代码状态与实际进入模型输入的 LLM context。OpenAI Agents SDK Sessions用于区分会话历史持久化与独立业务任务状态。本篇主要内容第 11 篇用RecoveryState让一次 Agent Loop 能从截断、上下文过长和暂时限流中恢复但恢复状态只服务当前运行任务目标仍依赖对话历史。本篇新增Task数据结构和.tasks文件目录用blockedBy表达依赖用pending → in_progress → completed表达生命周期再把创建、查询、认领和完成注册成工具最后验证依赖解锁并说明循环依赖、并发覆盖、owner 失效等生产缺口。下篇预告Task 已经能够跨轮保存但慢命令仍会占住 Agent Loop。第 13 篇将把长操作移入后台并把完成通知重新注入前台消息。一、一次运行能够恢复为什么任务目标仍会随对话结束第 11 篇把错误分成输出截断、输入过长、暂时限流和不可恢复错误由RecoveryState控制 token 上调、续写、响应式压缩、退避与 fallback。它解决的是“当前模型调用怎样继续”并没有回答“跨越多个轮次、多个进程的目标保存在哪里”。假设任务是先生成接口定义接口定义完成后再实现客户端最后运行集成检查。只把这三步写在messages里会产生三个问题。第一响应式压缩可能删除早期目标。第二终端关闭后内存历史消失。第三模型每次都要从自然语言重新判断哪一步完成、哪一步阻塞状态没有可靠事实源。Memory 可以保存长期偏好和经验但不适合承担每个任务的实时状态机。第 12 篇因此把“对话”和“任务”拆成两条生命周期不同的数据messages保存模型本轮需要理解的交流、工具调用和结果Task 文件保存目标、依赖、owner 与状态模型需要任务事实时通过工具读取Harness 修改任务时写回独立存储而不是改写聊天文本。两条生命周期的差异要从“终点”观察上方 messages 可能因压缩或会话结束不再完整下方 Task 状态经过 JSON 写入后能够在进程重启时重新加载。它们可以相互引用但不能互相充当唯一事实源。OpenAI Agents SDK 的 Context Management 文档区分本地代码可见 context 与模型可见 contextSessions 文档则说明 session 会在多次 run 之间保存 conversation history。两者都能帮助理解边界持久会话保存的是交流历史Task System 保存的是业务目标状态。即使二者最终都落在数据库中也不应该用一张表或同一段文本混成一个概念。s12_task_system.py为了集中讲任务系统保留了基础 Agent Loop、Prompt 组装、文件工具和 Tool Calling却明确省略第 11 篇完整的RecoveryState、退避、上调和压缩代码。这不是说 Task System 替代 Error Recovery而是教学快照只展开本篇增量。真实 Harness 中两层应组合恢复层包住模型或外部调用任务层独立维护目标状态。左侧恢复层与右侧任务层都接在 Agent Loop 上却维护不同对象。恢复层回答“本次调用怎样继续”任务层回答“目标当前进行到哪里”省略一段展开代码不等于删除该层的架构职责。因此阅读第 12 份源码时不应得出“恢复能力消失”的结论。代码快照为了突出增量而简化组合架构上的两个模块仍可通过同一个 Agent Loop 和 handler 边界共同工作。二、持久任务图由哪些对象组成Task 的最小对象有六个字段。源码用dataclass声明id: str、subject: str、description: str、status: str、owner: str | None与blockedBy: list[str]让字段定义、初始化参数和序列化对象保持对应。这里先看字段之间的职责分区再进入文件读写代码。id提供稳定引用subject是短目标description保存补充说明status表示生命周期owner表示当前责任主体blockedBy保存必须先完成的任务 ID。对象图把身份与引用放在蓝色区域把状态和目标放在绿色区域把 owner 与依赖分别作为责任和图关系观察点。dataclass根据类型标注生成初始化和表示等方法代码再用asdict(task)转成可序列化字典。六个字段中只有description主要服务自然语言其余字段都参与程序判断或定位。模型可以提出 subject 与 descriptionID、状态转移、owner 身份和依赖合法性则应由 Harness 生成或校验不能全部接受模型自由填写。这里有两个细节。blockedBy是可变列表示例在create_task()中用blockedBy or []为每个 Task 创建新列表没有把同一个默认列表共享给所有实例。若直接写blockedBy: list[str] []dataclass 会拒绝这种可变默认值标准写法是field(default_factorylist)。第二status只是普通字符串。注释约定它只能是pending、in_progress、completed但类型系统和加载函数没有校验。任何 JSON 都可以写入status: runninggg。教学代码通过少量分支维持约定生产实现应使用 Enum、Literal 或验证模型并对存量文件执行 schema 校验。持久化层把每个 Task 写成.tasks/{task_id}.jsonTASKS_DIRWORKDIR/.tasksTASKS_DIR.mkdir(exist_okTrue)def_task_path(task_id:str)-Path:根据任务 ID 返回对应 JSON 文件路径。returnTASKS_DIR/f{task_id}.jsondefsave_task(task:Task):把任务当前状态写入独立 JSON 文件。_task_path(task.id).write_text(json.dumps(asdict(task),indent2))defload_task(task_id:str)-Task:从 JSON 文件恢复 Task 对象。datajson.loads(_task_path(task_id).read_text())returnTask(**data)文件使 Task 脱离 Python 进程退出程序后再次启动只要工作目录与.tasks保留list_tasks()仍能发现它们。它也便于观察教学状态每次认领或完成都会直接反映到 JSON。这条链的输入和输出都是 Task 对象中间 JSON 只是当前教学存储格式。只要 repository 接口保持save/load/list语义未来换成数据库不会改变上层状态机反过来若业务代码到处直接拼文件路径存储替换就会扩散到所有 handler。但“写到磁盘”只代表持久化不代表可靠事务。write_text()直接覆盖目标文件进程在写入中途崩溃可能留下不完整 JSON两个 Agent 同时修改同一 Task 会发生后写覆盖前写。生产实现至少要临时文件写入后原子替换、文件锁或数据库事务、版本号或 compare-and-swap并为损坏记录提供隔离与恢复。多个 Task 通过blockedBy形成有向依赖图。若 B 的blockedBy[A]B 只有在 A 的状态是completed时才可认领。can_start()就是这条边的运行判断defcan_start(task_id:str)-bool:确认所有前置任务都存在且已经完成。taskload_task(task_id)fordependency_idintask.blockedBy:ifnot_task_path(dependency_id).exists():returnFalseifload_task(dependency_id).status!completed:returnFalsereturnTrue缺失依赖被视为阻塞而不是自动忽略这能避免 B 在前置事实未知时提前开始。Task Graph 的关键不是画出很多方框而是让“某条边的前置状态是否满足”成为可重复计算的规则。依赖边只传递“是否允许开始”不会自动执行下游。A 完成后 B 从 blocked 变成可认领仍要经过claim_task()才进入in_progressB 完成后 C 才获得相同机会。这种逐边解锁避免模型仅凭一句“前置应该完成了”跳过事实检查。当前代码没有检查环。如果 A 阻塞于 BB 又阻塞于 A二者永远不能开始如果 Task 把自己放进blockedBy结果相同。创建任务时应该验证依赖存在、禁止自环并对整个图执行 cycle detection。缺失依赖、循环依赖和正常等待都表现为can_startFalse若不返回具体原因排障会非常困难。三、任务怎样创建、解锁并重新进入Agent Loop状态机只有三步pending已经创建尚未认领→in_progress依赖完成某个 owner 已认领→completed工作完成下游可能解锁claim_task()同时检查当前状态和依赖然后写入 ownercomplete_task()只允许从in_progress进入completed保存后扫描其他 pending Task报告刚刚解锁的下游defclaim_task(task_id:str,owner:stragent)-str:认领可启动任务并记录 owner。taskload_task(task_id)iftask.status!pending:returnfTask{task_id}is{task.status}, cannot claimifnotcan_start(task_id):returnfTask{task_id}is blockedtask.ownerowner task.statusin_progresssave_task(task)returnfClaimed{task.id}defcomplete_task(task_id:str)-str:完成任务并计算被解锁的下游任务。taskload_task(task_id)iftask.status!in_progress:returnfTask{task_id}is{task.status}, cannot completetask.statuscompletedsave_task(task)unblocked[item.subjectforiteminlist_tasks()ifitem.statuspendinganditem.blockedByandcan_start(item.id)]returnfCompleted{task.id}; Unblocked:{unblocked}这段代码真正改变了三个事实认领时 status 与 owner 一起写入完成时只允许从in_progress转移完成后重新计算下游can_start()。状态判断发生在 handler 内模型即使要求“直接完成 pending Task”也只会得到拒绝文本。状态机需要重点观察blockedBy分支与 owner 标签。依赖未完成时Task 仍处于 pending只是暂时不可认领owner 只在认领成功后出现并随 Task 一起持久化。owner 在这里提供了“谁正在处理”的可观察字段但还不是所有权约束。complete_task()没有接收当前 Agent 身份也没有验证完成者是否等于task.owner任何能调用工具的 Agent 都可以完成别人的任务。后续多 Agent 章节会继续扩展 owner而生产系统现在就应该把调用者身份交给 handler在服务端校验不能只依赖模型自觉传正确名字。为了让模型使用 Task System代码把五个本地 handler 注册为工具create_task、list_tasks、get_task、claim_task、complete_task。OpenAI-compatible Chat Completions 只承载函数名和 JSON 参数任务对象、JSON 文件、依赖检查与状态变更都留在 Harness。TOOLS[{name:create_task,description:Create a task with blockedBy dependencies.,parameters:{type:object,properties:{subject:{type:string},description:{type:string},blockedBy:{type:array,items:{type:string},},},required:[subject],},},# list_tasks / get_task / claim_task / complete_task 同样注册]TOOL_HANDLERS{create_task:run_create_task,list_tasks:run_list_tasks,get_task:run_get_task,claim_task:run_claim_task,complete_task:run_complete_task,}这段注册表只把稳定工具名映射到本地 handler没有把文件路径或 Task 对象直接暴露给模型。模型输出claim_task(task_id)后Harness 才解析参数、查找 handler、读取 JSON、验证状态并形成 tool result。协议交接需要沿箭头检查两种数据向右传的是函数名与 JSON 参数向左回到 messages 的是执行结果真正的状态写入始终停留在 Harness 一侧。因此Task System 不是 Chat Completions 提供的远程任务服务。兼容协议只让模型表达结构化意图.tasks的目录位置、并发策略、权限和生命周期都是应用自己的设计。Task 状态不会因为存在于.tasks就自动进入模型上下文。模型只有调用list_tasks或get_task并收到roletool结果后才看见它。也可以由 Harness 在每轮调用前选择少量相关任务注入 system但必须控制数量、权限和新鲜度。把整个任务库无条件塞进 Prompt 会重演第 08 篇的上下文膨胀问题。不调用模型端点也能对依赖解锁做本地状态推演步骤操作A 状态B 状态B 能否认领1创建 Apending不存在—2创建 BblockedBy[A]pendingpending否3认领 Ain_progresspending否4完成 Acompletedpending是5认领 Bcompletedin_progress已认领这项推演验证的是can_start()、claim_task()与complete_task()的本地控制流。它不验证模型是否总能规划出正确依赖也不验证并发进程下文件是否安全。模型负责提出任务操作Harness 必须独立校验状态转移。前后快照需要观察 B 的两次字段变化执行complete_task(A)后B 仍是 pending但can_start从假变为真执行claim_task(B)后B 才进入in_progress。把二者合并会让调度与执行责任混在一起。这一分离也为后续后台任务留下空间scheduler 可以寻找can_startTrue的 pending Taskworker 再以自己的身份认领没有 worker 时任务保持可认领而不会被误记为执行中。解锁事件还可以成为审计记录或通知但它本身不应该替 worker 完成认领。保留这个中间状态系统才能在多个可用 worker 之间执行权限、容量和优先级判断。四、文件任务图为什么还不是生产调度系统文件版 Task System 已经具备数据、依赖、状态、owner、持久化和工具入口却仍不是队列、工作流引擎或调度器。它不会主动选择任务不会启动 worker不会在未来时间唤醒也不会在 Agent 崩溃后重新分配in_progress任务。第 13 篇和第 14 篇才会分别增加后台运行与时间触发。当前实现至少存在八类工程缺口。ID 可能碰撞。task_{秒级时间戳}_{0000..9999}在高并发下不能保证唯一碰撞会覆盖已有文件。应使用 UUID、数据库序列或带唯一约束的 ID。写入不是原子的。write_text()可能留下半截 JSON并发写会丢更新。应使用临时文件与原子替换或交给带事务和锁的存储。没有版本控制。两个 owner 先后读取相同状态并写回时系统无法发现陈旧写入。可以增加version字段并做乐观锁。依赖没有图校验。自环、循环依赖、指向已删除任务的边都只表现为永远阻塞。创建与更新时需要验证 DAG并返回可诊断路径。owner 只是标签。认领没有租约完成不核对调用者Agent 崩溃后任务永远停在in_progress。生产状态通常还需要 lease、heartbeat、超时与重新认领。状态种类不足。任务只能完成不能失败、取消、暂停或等待人工输入。实际系统至少需要失败原因、重试次数、下次运行时间与终止原因。坏文件会拖垮列表。list_tasks()对每个 JSON 直接反序列化一个损坏文件就可能让整个列表失败。需要逐条隔离、schema 校验、错误目录和修复工具。权限边界缺失。Task ID 能直接映射文件名虽然示例 ID 由程序生成handler 仍应校验格式、workspace 与调用者权限避免任意路径、越权查询或跨项目修改。六条风险说明单文件方案的主要问题不是“性能不够快”而是缺少一致性和可恢复性。唯一 ID 解决引用碰撞原子写与事务解决中途失败版本锁解决陈旧覆盖DAG 校验解决永久阻塞身份校验约束 owner逐条隔离避免一份坏数据拖垮全局查询。从教学实现走向生产系统可以把职责拆成四层Task repository 负责原子读写和版本Task service 负责状态转移、依赖与权限scheduler 负责选择何时可运行worker 负责执行并汇报心跳。Tool handler 只调用 service不直接操作文件。这样未来换成 SQLite、PostgreSQL 或队列时Agent Loop 和 Tool Schema 不必重写。repository 的接口还应明确失败语义找不到 Task、版本冲突、依赖未完成、租约过期和记录损坏必须返回不同错误。若它们都变成Error: cannot complete上层恢复机制就无法判断应该重试、刷新状态、重新认领还是转人工。Task 也不应该完全相信模型参数。blockedBy中的 ID 必须存在且在同一作用域owner应来自认证上下文而不是任意字符串complete应携带预期版本或租约任务描述可能包含敏感信息返回给模型前要做权限与最小披露。Prompt 可以提示规则真正的拒绝必须发生在 handler 或服务层。第 12 篇的完整增量是用Taskdataclass 固定目标对象用.tasksJSON 让状态跨进程保存用blockedBy计算依赖用pending → in_progress → completed表达最小生命周期再通过五个工具把模型意图接到 Harness 的任务服务。messages仍负责交流Task 文件负责业务事实两者按需交接而不是互相替代。这套结构已经能回答“任务是什么、由谁处理、为什么被阻塞、完成后解锁了谁”却不能让慢任务自动在后台运行。若bash执行十分钟当前agent_loop()仍会同步等待期间无法接收新输入也无法可靠处理中断。第 13 篇将把长操作移出前台循环增加后台进程与完成通知再把结果作为新 observation 注入 Agent。