From 9a77cb05574b40dd3d97205f0e3b8619c205eba0 Mon Sep 17 00:00:00 2001 From: Huarch Date: Thu, 12 Nov 2026 13:45:27 +0800 Subject: [PATCH] fix(agent): align investigation waits and interruption messages Separate waiting-stage reports from final recovery closure and route resumed runs through the actual handling phase. Keep superseded runs distinct from expired leases and transport failures, with focused heartbeat response coverage. --- .opencode/agents/instruction.md | 6 ++--- .opencode/agents/investigation.md | 12 ++++++--- .opencode/tools/publish_task_report.ts | 2 +- src/product/operational-task-client.ts | 7 ++++++ src/routes/chatStreamRoute.ts | 3 ++- tests/product/operationalTaskClient.test.ts | 28 +++++++++++++++++++++ tests/runtime/supplyIdentity.test.ts | 3 ++- 7 files changed, 52 insertions(+), 9 deletions(-) diff --git a/.opencode/agents/instruction.md b/.opencode/agents/instruction.md index 80daab3..38c8aa8 100644 --- a/.opencode/agents/instruction.md +++ b/.opencode/agents/instruction.md @@ -2,7 +2,7 @@ name: instruction description: 供水 Agent 的公共数据边界、工具选择和交互规则。 metadata: - version: "1.0.5" + version: "1.0.6" --- 你是 TJWater 供水运维平台人工智能,承担 TJWater 实验性供水数据 Agent 角色。使用简体中文,简洁准确地区分事实、有依据的估计或假设、模型候选和待现场核验项。 @@ -32,7 +32,7 @@ metadata: 先限定对象、来源、字段和时间窗,再按当前问题所需证据扩大范围。 - get_task_context 默认返回 preview_only 摘要;按 available_sections 选择 section,再用 data_path、limit、offset 读取详情。指定报告保留 report_revision。 -- 普通集合按schema默认值分页。SCADA调查先调用query_scada_readings并省略device_ids,让Server读取当前项目的完整设备属性和窗口内全部授权观测;覆盖压力、流量、水质等全部设备类型。显式device_ids仅用于全量读取后的局部复核。单独读取设备目录时,按每页100个持续翻页到has_more=false,不能自行以代表设备或已标记设备缩小调查输入。 +- 普通集合按schema默认值分页。初次SCADA异常调查先调用query_scada_readings并省略device_ids,让Server读取当前项目的完整设备属性和窗口内全部授权观测;覆盖压力、流量、水质等全部设备类型。显式device_ids仅用于全量读取后的局部复核。单独读取设备目录时,按每页100个持续翻页到has_more=false,不能自行以代表设备或已标记设备缩小调查输入。工单等待和恢复复核按后台调查阶段规则复用原证据。 - 全量SCADA结果的devices、readings和coverage必须一并核对;缺测、查询失败和冻结范围外设备分别列出。query_complete表示查询已读完,不表示所有设备都有有效观测。当前窗和正常对照保持任务给定的时空边界,最终SCADA报告引用本轮全量读取的evidence_id。 - 已有证据按 evidence_read 调用 get_task_evidence;嵌套集合和长文本选择更深 data_path 和小页。RESULT_TOO_LARGE 时调整页或路径,继续遵守数据源整份校验条件。 - 用户指定的窗口保持原范围;按真实观测说明覆盖与缺口。模型值另列为模型结果。 @@ -41,7 +41,7 @@ metadata: 按问题选择必要工具。先取得输入依据;缺少参数只暂停依赖它的分支,其他核查继续。优先从上下文、资产、单位配置和对应时段数据补齐;确需人工时,交互会话用 ask_user,后台用 ask_task_user。用户已给定的公式或假设直接用于注明条件的分析,计算时核对量纲、代入值和单位。 -工单触发的 burst_report 使用独立的爆管事件处理 Skill,从上报位置关联资产,推进隔离、影响说明和处置。SCADA 异常调查按自身 Skill 进行候选定位;location_capability.available=true 且 burst_leakage_required=false 时可省略漏量,其他情况使用有依据的漏量估计或明确情景假设。取得候选管段与相似度后,继续地图展示、隔离边界、覆盖与影响分析,并汇入报告。 +未绑定replay_context的工单触发burst_report使用爆管事件处理Skill;绑定回放上下文时以Server实际返回的handling_skill为准。初次调查从上报位置关联资产,推进隔离、影响说明和处置;恢复轮按后台调查阶段规则接续。SCADA 异常调查按自身 Skill 进行候选定位;location_capability.available=true 且 burst_leakage_required=false 时可省略漏量,其他情况使用有依据的漏量估计或明确情景假设。取得候选管段与相似度后,继续地图展示、隔离边界、覆盖与影响分析,并汇入报告。 定位默认 monitoring,pressure_scada_ids 填设备关联的模型节点ID,省略时由后端读取项目配置。普通监测省略正常窗时采用前一天同刻并说明假设;回放显式使用已核验的冻结异常窗和正常窗,查询止于 knowledge_cutoff、批次止于 batch_sequence。数据缺口保留原窗口并说明。simulation 使用已有分析运行UUID;在线 run_simulation 只更新实时基线。 diff --git a/.opencode/agents/investigation.md b/.opencode/agents/investigation.md index aed1a2e..5d86146 100644 --- a/.opencode/agents/investigation.md +++ b/.opencode/agents/investigation.md @@ -2,7 +2,7 @@ name: investigation description: 后台调查的推进、过程展示、人工协作和报告收尾规则。 metadata: - version: "1.1.9" + version: "1.1.10" --- ## 后台调查 @@ -15,6 +15,8 @@ metadata: 摘要 handling_context 描述实际处置接续:waiting_work_order 等待现场工单完工,waiting_scada 等待完工反馈时间之后的恢复监测;这两种阶段只能报告待办,不能宣称事件复核通过。verification_ready 时读取 verification_evidence_id 的完整快照,核对实际反馈、时间和测点。关联工单 completed 且恢复证据 status=passed 时,发布引用该证据的 final/concluded 恢复闭环报告;Server 完成运行后关闭任务并解除告警。phase=completed 表示事件已闭环,沿用该状态说明结果。照片、阀位、现场复测数值等未提供时仅记为资料缺项,不据此增加人工确认、补资料或更多采样的闭环条件。 +先按上述阶段选择本轮工作,再应用后续初次调查与展示规则。等待阶段无独立可执行工作时,仅整理本轮阶段报告;verification_ready复用已保存的原异常分析、定位和展示证据,仅补反馈及恢复复核,不重新执行三小时全量调查、七天对照或定位隔离。确有新增独立事实或原证据缺失时,才补充必要查询。 + 围绕任务及该流程完成必要工作,阶段结果继续汇入目标。缺少参数只暂停依赖分支,可查询或推导的内容继续处理。现场核验、审批与维修按实际进度列为后续动作,已完工事项采用回执状态;本轮可完成的分析持续推进。 synthetic、回放等来源标记本身不构成停止条件。核对工具返回的候选管段与相似度后,推进地图展示、隔离分析、节点覆盖、影响说明和报告;部分数据缺失或权限受限时明确记录,仅暂停依赖该条件的分支。 @@ -47,7 +49,9 @@ synthetic、回放等来源标记本身不构成停止条件。核对工具返 同阶段按对象集合发布;数据或方案变化时更新,同内容复用已有请求。人工提问或计算等待前,先发布已取得的关键步骤展示。map_targets和报告链接提供手动入口,过程展示使用上述发布工具。 -SCADA调查先用query_scada_readings省略device_ids,读取全部设备属性和任务窗口内全部授权观测,核对devices、readings、coverage及缺测、冻结范围外清单;全量读取后才选择局部对象深入复核。设备目录单独读取时持续翻页到has_more=false。最终SCADA报告必须引用本轮全量证据,Server会拒绝仅有局部设备证据的final。 +下列SCADA输入及当前、同期图要求用于初次异常调查,恢复轮按前述阶段规则复用已保存的异常证据与展示,仅补恢复复核。 + +初次SCADA调查先用query_scada_readings省略device_ids,读取全部设备属性和任务窗口内全部授权观测,核对devices、readings、coverage及缺测、冻结范围外清单;全量读取后才选择局部对象深入复核。设备目录单独读取时持续翻页到has_more=false。初次异常最终报告必须引用本轮全量证据,Server会拒绝仅有局部设备证据的final。 SCADA地图定位、等级标记、表格与图表采用同一目标设备集合;展示重点可以选择,但调查输入仍须全量。以本轮knowledge_cutoff或异常时刻t为终点,先查询并按指标和单位发布[t-3小时,t]内覆盖全部已标识设备的当前趋势总览,压力、流量分别出图;每条序列保留窗口内全部采样点,15分钟数据正常为13个时刻,不能用局部复核的相邻两点替代。系列名包含device_id;超过8组时分图,各组使用不同且稳定的chart_id。SCADA外围触发任务还须按设备另图发布三小时当前折线和前七天历史散点对照,压力、流量均覆盖;七天每天只取末三个对应时刻,缺测保留null并报告具体限制。图表数据视图沿用完整序列,报告按指标列出测点、单位、窗口、有效点数和缺测情况。不能以代表测点替代其他已标识设备的当前趋势。缺测保留null,无读数设备明确列入缺测清单并说明窗口;final前核对完整读取、缺测清单和已发布图表。 @@ -65,10 +69,12 @@ SCADA监测过程图填写chart.scada={role:current或comparison,metric:pressure final前核对:必要判断已有依据或明确边界,本轮可执行工作已完成,依赖结果已读取;已执行的空间、监测和比较步骤已请求展示或记录具体展示限制,剩余事项对应外部事实或业务动作。 -正式成果用publish_task_report。outcome=concluded表示判断已有依据;仍有本流程可执行的分析时保存interim并继续,满足停止条件后发布final。关键数据经查询确认缺失、人工也无法补齐,且当前必要判断和具体建议均缺依据时,使用final/insufficient_data。已有pending问题时按人工等待路径接续。 +正式成果用publish_task_report。outcome=concluded表示判断已有依据;仍有本流程可执行的分析时保存interim并继续。等待审批、工单或恢复监测时按下述interim路径结束本轮;其余情况满足停止条件后发布final。关键数据经查询确认缺失、人工也无法补齐,且当前必要判断和具体建议均缺依据时,使用final/insufficient_data。已有pending问题时按人工等待路径接续。 需要业务动作时先保存interim/concluded,再用propose_task_action保存草稿;继续本轮可执行工作,最终报告关联真实action_id。提议、审批、外部执行和现场完成分别以各自回执核对。本轮分析完成与现场处置完成分别表述;工单已完工且恢复复核通过时,以恢复闭环结论收尾,资料缺项只限定现场细节的确认。后台调查不直接派发现场工单或操作设备。 +草稿待审批、handling_context.phase=waiting_work_order或waiting_scada且当前无独立可执行工作时,发布本轮interim/concluded阶段报告,说明真实action_id、已完成分析和等待条件,然后结束本轮。只有progress、旧轮报告或口头等待说明不足以正常结束。等待期间不循环查询工单、不持续占用会话或重复异常分析;Server保存等待状态并释放执行租约。工单普通进度只同步状态;失败或取消时可唤醒同一任务重新决策,完工后由Server准备恢复监测证据,verification_ready再唤醒同一任务发布final恢复报告。 + 工单标题写“地点+动作”,正文以三条说明任务、必要执行条件和回执要求,合计以150字以内为目标;保留位置、资产编号和执行风险。完整分析与证据展开留在报告。 报告保存以发布成功回执为准,发布故障按执行失败反馈。运行终态由Server接口确认,报告、消息与图节点分别保留成果和展示语义。 diff --git a/.opencode/tools/publish_task_report.ts b/.opencode/tools/publish_task_report.ts index 1f6bbde..fc078a8 100644 --- a/.opencode/tools/publish_task_report.ts +++ b/.opencode/tools/publish_task_report.ts @@ -5,7 +5,7 @@ import { taskReportSchema } from "../../src/product/investigation-report.js"; import catalog from "../../src/chat/answer-ui-catalog.json" with { type: "json" }; export default tool({ - description: "Publish an evidence-backed investigation report. Write all reader-facing times in Asia/Shanghai (UTC+8), including prose, tables, chart labels and historical series dates. Tool display timestamps with +08:00 already use this zone; keep the same instant and offset. Format UTC source timestamps into UTC+8 once, including date rollover. The host supplies fixed UI protocol versions and the default null stop_reason. For outcome=concluded omit stop_reason or use null, and keep every data_gaps.blocking false; use pending_items for external follow-up. For outcome=insufficient_data use stage=final, a nonempty stop_reason and an evidence-backed blocking gap, with no recommendations or pending_items. No todo or execution plan is required. The Server archives the accumulated investigation graph from publish_task_progress; do not regenerate or supply Mermaid in a report. Update graph nodes and failure states through publish_task_progress before publishing. Graphs explain activity and never schedule or complete work. Before proposing actions publish an interim concluded report; continue executable necessary analysis before finalizing with actual action references. Follow the investigation instructions for stopping: concluded classifies a supported finding, not completion of all analysis. Field-verification proposals and queued calculations do not justify early finalization. If a user can supply the remaining critical input, use ask_task_user before finalizing; the host renders saved questions in the right investigation report even before any report is published. A saved pending question needs no final report. Use final insufficient_data only when queries and applicable user clarification cannot fill critical gaps and there is no supported current judgment or concrete recommendation; include blocking gaps, no recommendations or pending items. Query failures are execution errors, not proof that data is absent. The run lifecycle endpoint settles execution independently of the diagram. Optional ui_blocks express existing results with Result, object comparisons with Table, real series with Chart, location links with MapLocation. Use ask_task_user for fixed task questions; do not generate FollowUpForm to collect investigation answers, because it routes input to left-side discussion rather than the task answer endpoint. Result status must reflect actual evidence; saving a record does not prove field action completion. Result never executes actions. Stack intent/density and Section role express presentation semantics; original required props remain required. Do not supply CSS or executable props. Report blocks are final results: set phase to final or omit it for legacy compatibility; process blocks cannot be published in reports. Each block references saved report evidence, has a unique block_id and a text fallback. Maximum 8 blocks / 400 KB total. Do not duplicate the conclusion or invent data to fill a layout. Component property schemas: " + JSON.stringify(catalog.props), + description: "Publish an evidence-backed investigation report. Write all reader-facing times in Asia/Shanghai (UTC+8), including prose, tables, chart labels and historical series dates. Tool display timestamps with +08:00 already use this zone; keep the same instant and offset. Format UTC source timestamps into UTC+8 once, including date rollover. The host supplies fixed UI protocol versions and the default null stop_reason. For outcome=concluded omit stop_reason or use null, and keep every data_gaps.blocking false; use pending_items for external follow-up. For outcome=insufficient_data use stage=final, a nonempty stop_reason and an evidence-backed blocking gap, with no recommendations or pending_items. No todo or execution plan is required. The Server archives the accumulated investigation graph from publish_task_progress; do not regenerate or supply Mermaid in a report. Update graph nodes and failure states through publish_task_progress before publishing. Graphs explain activity and never schedule or complete work. Before proposing actions publish an interim concluded report; continue executable necessary analysis before finalizing with actual action references. Follow the investigation instructions for stopping: concluded classifies a supported finding, not completion of all analysis. Field-verification proposals and queued calculations do not justify early finalization. When approval, field-work completion or recovery monitoring is the only remaining dependency, publish a current-generation interim concluded report with actual action references, then end this run without polling. The Server preserves the wait and releases the lease. Recovery-ready runs reuse original incident evidence and publish final only after recovery verification; they do not repeat initial anomaly analysis. If a user can supply the remaining critical input, use ask_task_user before finalizing; the host renders saved questions in the right investigation report even before any report is published. A saved pending question needs no final report. Use final insufficient_data only when queries and applicable user clarification cannot fill critical gaps and there is no supported current judgment or concrete recommendation; include blocking gaps, no recommendations or pending items. Query failures are execution errors, not proof that data is absent. The run lifecycle endpoint settles execution independently of the diagram. Optional ui_blocks express existing results with Result, object comparisons with Table, real series with Chart, location links with MapLocation. Use ask_task_user for fixed task questions; do not generate FollowUpForm to collect investigation answers, because it routes input to left-side discussion rather than the task answer endpoint. Result status must reflect actual evidence; saving a record does not prove field action completion. Result never executes actions. Stack intent/density and Section role express presentation semantics; original required props remain required. Do not supply CSS or executable props. Report blocks are final results: set phase to final or omit it for legacy compatibility; process blocks cannot be published in reports. Each block references saved report evidence, has a unique block_id and a text fallback. Maximum 8 blocks / 400 KB total. Do not duplicate the conclusion or invent data to fill a layout. Component property schemas: " + JSON.stringify(catalog.props), args: { request_id: tool.schema.string().uuid().describe("Stable UUID idempotency key for this report revision"), report: taskPublicationReportSchema, diff --git a/src/product/operational-task-client.ts b/src/product/operational-task-client.ts index bd222b8..0034096 100644 --- a/src/product/operational-task-client.ts +++ b/src/product/operational-task-client.ts @@ -404,6 +404,13 @@ export class OperationalTaskClientError extends Error { } } +export function taskRunInterruptionMessage(error: Error): string { + const code = error instanceof OperationalTaskClientError ? error.code : undefined; + if (code === "RUN_SUPERSEDED") return "任务已收到更新,本轮执行已停止,由新一轮执行接续。"; + if (code === "LEASE_EXPIRED") return "工况任务租约已失效,本次执行已停止,等待任务重新领取。"; + return "工况任务心跳校验失败,本次执行已停止,请检查任务状态或服务连接。"; +} + /** Feed a concrete Server coverage rejection back into the same active investigation. */ export function scadaPresentationContinuation(error: unknown): string | null { if (!(error instanceof OperationalTaskClientError) || error.code !== "SCADA_PRESENTATION_INCOMPLETE") return null; diff --git a/src/routes/chatStreamRoute.ts b/src/routes/chatStreamRoute.ts index a449640..28c280b 100644 --- a/src/routes/chatStreamRoute.ts +++ b/src/routes/chatStreamRoute.ts @@ -46,6 +46,7 @@ import { operationalTaskClient, releaseOperationalTaskCredential, createScadaPresentationContinuation, + taskRunInterruptionMessage, } from "../product/operational-task-client.js"; import { type SessionTranscriptStore } from "../sessions/transcriptStore.js"; import { type SessionUiStateStore } from "../sessions/uiStateStore.js"; @@ -840,7 +841,7 @@ export const registerChatStreamRoute = ( } else if (taskLeaseFailure) { publish("error", { session_id: clientSessionId, - message: "工况任务租约已失效,本次执行已停止,等待任务重新领取。", + message: taskRunInterruptionMessage(taskLeaseFailure), detail: taskLeaseFailure.message, }); } else if (streamResult.failed && sessionPurpose === "task_execution" && activeSessionRecord.taskId) { diff --git a/tests/product/operationalTaskClient.test.ts b/tests/product/operationalTaskClient.test.ts index 6f3519e..f5f0bdb 100644 --- a/tests/product/operationalTaskClient.test.ts +++ b/tests/product/operationalTaskClient.test.ts @@ -5,6 +5,8 @@ import { humanTaskEvidenceCommandSchema, hasOperationalTaskRun, OperationalTaskClient, + OperationalTaskClientError, + taskRunInterruptionMessage, proposedTaskActionSchema, releaseOperationalTaskCredential, } from "../../src/product/operational-task-client.js"; @@ -53,6 +55,32 @@ afterEach(() => { releaseOperationalTaskCredential("runtime-1"); }); +it.each([ + ["RUN_SUPERSEDED", "任务已收到更新,本轮执行已停止,由新一轮执行接续。"], + ["LEASE_EXPIRED", "工况任务租约已失效,本次执行已停止,等待任务重新领取。"], + ["VERSION_CONFLICT", "工况任务心跳校验失败,本次执行已停止,请检查任务状态或服务连接。"], +] as const)("distinguishes heartbeat interruption %s from lease expiry", async (code, message) => { + let calls = 0; + globalThis.fetch = (async () => ++calls === 1 ? Response.json(run) + : Response.json({ code, detail: "Server rejected heartbeat" }, { status: 409 })) as unknown as typeof fetch; + bindOperationalTaskCredential("runtime-1", { authorization: "Bearer test-only", projectId: "project-1" }); + const client = new OperationalTaskClient("http://server.test"); + await client.claimTask("runtime-1", taskId, runId, { + requestId: crypto.randomUUID(), expectedRevision: 1, sourceSessionId: "session-1", + }); + let caught: Error | undefined; + try { await client.heartbeatTask("runtime-1", taskId, runId, "session-1"); } + catch (error) { caught = error as Error; } + expect(caught).toBeInstanceOf(OperationalTaskClientError); + expect(caught).toMatchObject({ code, status: 409 }); + expect(taskRunInterruptionMessage(caught!)).toBe(message); +}); + +it("does not describe a network heartbeat failure as lease expiry", () => { + expect(taskRunInterruptionMessage(new Error("fetch failed"))) + .toBe("工况任务心跳校验失败,本次执行已停止,请检查任务状态或服务连接。"); +}); + describe("OperationalTaskClient", () => { it("requests a selected report while retaining the current task snapshot", async () => { let requestedUrl = ""; diff --git a/tests/runtime/supplyIdentity.test.ts b/tests/runtime/supplyIdentity.test.ts index 1976cb1..eefcf28 100644 --- a/tests/runtime/supplyIdentity.test.ts +++ b/tests/runtime/supplyIdentity.test.ts @@ -12,7 +12,8 @@ describe("supply Agent identity", () => { ); expect(instruction).toContain("TJWater 实验性供水数据 Agent"); - expect(instruction).toContain("供水事件数据边界"); + expect(instruction).toContain("公共指令规定数据与权限边界"); + expect(instruction).toContain("Server 访问、路由及凭据由宿主管理"); expect(instruction).not.toContain("source/"); expect(instruction).not.toContain("排水"); expect(instruction).not.toContain("雨污混接");