
oh-my-pi 后台任务结果交付async-result 注入协议与结构化输出机制详解【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pioh-my-pi⌥ Coding agent with the IDE wired in将耗时的子代理subagent工作委托为后台任务任务完成后的结果如何回到主代理的会话流中是其异步架构的关键一环。本文以 async-result.md 这份 Handlebars 系统提示模板为主体结合 async-job-delivery.ts、job-manager.ts 等源码完整讲解 async-result 消息的模板结构、渲染逻辑、结构化输出schema语义、agent:// 寻址方式与去重交付生命周期帮助你理解并复用这套后台任务完成通知协议。一、async-result 是什么一份注入到会话转录中的完成通知在 oh-my-pi 中task工具可以把工作委托给后台子代理异步执行详见 task.md 中的 Async Job Contract。当这些后台作业background job结束完成、失败或取消时系统会把一份格式化好的结果通知注入到主代理的会话转录transcript中作为后续轮次的输入这份通知就是async-result。它由 async-result.md 这份 Handlebars 模板定义模板头部包裹在system-notice标签内属于系统注入内容而非模型自身输出。从 async-job-delivery.ts 可以看到它的类型常量export const ASYNC_RESULT_MESSAGE_TYPE async-result;该消息以customType async-result的 custom 角色消息形式注入转录async-job-delivery.ts携带display: true与attribution: agent并且会附带后台任务运行中恢复出的图片内容entry.job?.latestDetails?.images。二、模板逐段拆解单任务与多任务的两种呈现async-result 模板的核心逻辑依据multiple布尔变量分支。当jobs.length 1时进入多任务分支否则为单任务分支{{#if multiple}}{{jobs.length}} background jobs have completed. Resume your work using the results below. {{else}}Background job {{jobs.[0].jobId}} has completed. Resume your work using the result below. {{/if}}单任务直接点名jobId提示后台任务 {id} 已完成使用下面的结果继续你的工作多任务先给出完成数量N background jobs have completed并明确要求使用下面的结果继续工作Resume your work using the results below。随后模板遍历每个作业{{#each jobs}}。在多任务场景下每个作业前会输出以──开头的分隔标题包含作业 ID 和可选标签{{#each jobs}}{{#if root.multiple}}── Job {{this.jobId}}{{#if this.label}} ({{this.label}}){{/if}} ── {{/if}}{{this.result}}{{#if this.schemaStatus}}之后直接拼接this.result——这是作业结算settled时交付的正文文本。值得注意的是分隔标题只在multiple时出现单任务时直接输出结果正文。模板的数据来源在 buildAsyncResultBatchMessage 中构造每个作业条目携带jobId、result、typebash | task | eval、label、durationMs、structured、structuredJson、hasStructuredData、schemaStatus、schemaError、schemaValid等字段。其中schemaStatus由作业的 structured 输出状态valid/invalid等填充。三、结构化输出Structured Output的 schema 状态呈现async-result 模板的第二个核心区块负责呈现结构化输出的校验状态。模板原文为Structured output: schema {{this.schemaStatus}}{{#if this.schemaError}}: {{this.schemaError}}{{/if}}{{#if this.hasStructuredData}}; full payload at agent://{{this.agentUrlId}}, fields via agent://{{this.agentUrlId}}?q.field{{/if}}{{#unless this.schemaValid}}{{#if this.structuredJson}}; preview: json {{this.structuredJson}} {{/if}}{{/unless}}{{/if}}其语义可以拆解为四个层次schema 状态声明固定输出Structured output: schema {status}其中 status 来自作业的structured.status如valid/invalid错误信息若存在schemaError以冒号形式追加完整载荷寻址当hasStructuredData为真时给出两个 agent:// 寻址方式——agent://{id}读取完整 JSON 载荷agent://{id}?q.field按字段查询。这里的agentUrlId由entry.job?.agentId ?? entry.jobId决定async-job-delivery.ts因为任务作业的产物文件id.md/id.json写入的是子代理自身的 agent ID内联预览当 schema 校验不通过schemaValid为假且存在structuredJson时在 JSON 代码块中内联预览载荷。为什么无效结果才内联预览从 renderStructuredJson 的实现可以看出设计意图只有 schema 无效或出错的结果才会把 JSON 内联到通知正文中。有效结果直接指向agent://id——因为 sidecar 的output块已经承载了完整 JSON无需在通知里重复。这避免了有效载荷的冗余传输同时保证无效结果即模型无法直接通过 agent:// 读取的结构化数据仍然可见。内联预览本身受两个常量约束async-job-delivery.ts/** Result payloads longer than this spill to an artifact with an inline preview. */ export const ASYNC_INLINE_RESULT_MAX_CHARS 12_000; export const ASYNC_PREVIEW_MAX_CHARS 4_000;renderStructuredJson使用JSON.stringify(structured.data, null, 2)进行紧凑美观的序列化并通过truncateMiddle从中间截断至ASYNC_PREVIEW_MAX_CHARS4KB既保留头部与尾部信息又防止超长载荷撑爆上下文。四、agent:// 寻址与后台任务契约Async Job Contractasync-result 中大量出现的agent://id与history://id寻址其完整语义定义在配套的 task-async-contract.md 中全文如下No polling needed. Settled-job inspection: hub jobs | hub wait delivers its snapshot → no duplicate async-result. Job IDs: process memory ~5min after settlement; afterward use agent ID: hub send, agent://id, history://id. completed: subagent yielded successfully; claimed artifacts unverified.这份契约包含三条关键规则也在 task.md 的 Async Job Contract 与 hub.md 中得到呼应无需轮询No polling needed结果自动交付。hub jobs/hub wait的结算快照本身就是一次交付——如果先通过这两个操作观察到了已结算作业则该快照被视为交付并抑制后续重复的 async-resultJob ID 有效期约 5 分钟作业 ID 仅保存在进程内存中结算后约 5 分钟过期过期后应改用 agent ID通过hub send、agent://id或history://id访问completed≠ 验收通过completed仅表示子代理成功让出yield/ 作业正常退出声称产生的工件artifacts并未经过验证。主代理必须自行核查变更。五、交付生命周期从结算到注入的去重机制async-result 的生成与注入并非简单回调而是由 job-manager.ts 中一套完整的交付队列驱动。核心流程如下注册与执行register()创建AsyncJob含type、label、status、abortController、ownerId、agentId等字段作业运行结束后通过#enqueueDelivery(jobId, text)入队交付按归属路由交付时通过#resolveDeliverySink解析接收方。有 owner 的作业只路由到其 owner 注册的 delivery sinkregisterDeliverySink若 owner 没有活跃 sink 则 dead-letter丢弃并告警结果文本保留至 retention 过期——绝不会把某代理的结果泄漏到别的会话无 owner 的作业才使用默认onJobComplete回调重试与退避交付失败后按指数退避重试DELIVERY_RETRY_BASE_MS 500ms起步封顶DELIVERY_RETRY_MAX_MS 30s附加0~200ms抖动job-manager.ts抑制与消费标记acknowledgeDeliveries()/consumeJobResults()会把作业标记为已确认/已消费从而跳过排队中的交付#suppressedDeliveries与#consumedJobResults两个集合共同保证恰好一次交付——这正是hub jobs/hub wait快照抑制重复 async-result 的底层实现转录注入owner session 在构造时通过AsyncJobManager.registerDeliverySink注册自己的 sink见 async-job-delivery.ts 的模块注释完成的通知进入其 yield 队列由空闲 flush 作为后续轮次注入。代际epoch防护防止跨会话串扰AsyncResultEntry携带epoch字段async-job-delivery.ts记录入队时所属会话的 async-delivery 代数。会话发生/new、切换或 handoff 时代数递增因此代际不匹配的条目在 flush 时被丢弃——即使作业 ID 被复用清除了管理器的按 ID 抑制标记旧转录的结果也绝不会注入到新会话。六、与任务执行器的衔接supersede取代语义注入的 async-result 消息会被任务执行器识别。在 executor.ts 中/** * True when message is the session-injected async-result follow-up * ({link ASYNC_RESULT_MESSAGE_TYPE}): the transcript-ordered signal that a * background job outcome landed after whatever the model said before it. */ function isAsyncResultInjection(message: AgentMessage | undefined): boolean { return message?.role custom message.customType ASYNC_RESULT_MESSAGE_TYPE; }executor 的运行监视器run monitor通过匹配该 customType使先前记录的 yield让出失效在 yield 之后注入的结果会取代该 yield 原本携带的载荷见 async-job-delivery.ts 的注释说明。这保证了消息按转录顺序落位——无论模型在作业结算前说了什么async-result 都是其后的转录顺序信号。七、产物保留与清理让 agent:// 指针不悬空async-result 指向的agent://id背后是磁盘上的id.md/id.json产物文件。为了不让指针悬空job-manager 实现了**保留产物清理retained artifacts cleanup**机制默认 retention 为DEFAULT_RETENTION_MS 5 * 60 * 10005 分钟结算后作业行被调度淘汰但对于 detached spawn 的临时产物目录清理会延迟到 async-result 交付 settle 之后再加 60s 宽限RETAINED_ARTIFACTS_CLEANUP_GRACE_MS 60_000因为交付 sink 的收据在ASIDE_MESSAGE_COMMIT钩子触发后续消息插入转录时即已 resolve而模型下一次 provider 调用才能真正读到该指针宽限期正是为了覆盖这次往返job-manager.ts清理等待设有上界RETAINED_ARTIFACTS_CLEANUP_MAX_WAIT_MS与 retention 一致防止 sink 挂死导致临时目录泄漏清理失败仅记日志不阻塞作业淘汰或管理器销毁。八、测试验证仓库测试对 async-result 的行为有直接断言。hub-jobs-structured.test.ts 覆盖了hub wait/jobs/cancel的结构化输出渲染buildJobResult文件头注释明确breaks async-result.mds contract of pointing toagent://id即测试关注渲染结果是否遵守 async-result 契约中有效结果指向 agent://的约定。此外executor-async-quiescence.test.ts、agent-session-async-delivery.test.ts 等测试覆盖了交付、去重与 yield 取代等场景可作为深入研读该机制的入口。小结async-result 是 oh-my-pi 异步代理架构的最后一公里它以一份精炼的 Handlebars 模板async-result.md定义了单/多任务两种完成通知格式配合 async-job-delivery.ts 的批量消息组装、schema 状态呈现与 agent:// 寻址再叠加 job-manager.ts 的按归属路由、指数退避重试、去重抑制与产物保留清理共同实现了后台任务完成后自动、恰好一次、不跨会话串扰地把结果送回到正确的代理面前。理解这套机制也就理解了 oh-my-pi 中 task 批处理、hub 协作与结构化输出能够稳定运行的基础。【免费下载链接】oh-my-pi⌥ Coding agent with the IDE wired in项目地址: https://gitcode.com/GitHub_Trending/oh/oh-my-pi创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考