package runtime import ( agentllm "agent-native/runtime/llm" agenttools "agent-native/runtime/tools" core "agent-system-core" "bytes" "encoding/json" "fmt" "io" ) const workCheckpointTool = "publish_work_checkpoint" type WorkCheckpointSink interface { PublishWorkCheckpoint(core.WorkCheckpointCandidate) (string, error) } func checkpointEnabled(rc RunCtx) bool { if capability, ok := rc.(interface{ WorkCheckpointAvailable() bool }); ok && !capability.WorkCheckpointAvailable() { return false } src, ok := rc.(interface{ SubagentID() string }) if !ok || src.SubagentID() != materialSubagentID { return false } if _, ok = rc.(WorkCheckpointSink); !ok { return false } if !rc.Profile().Tools.ToolAllowed(workCheckpointTool) || rc.Profile().Tools.NeedsConfirm(workCheckpointTool) { return false } for _, tool := range rc.Tools() { if tool.Name == workCheckpointTool { return false } } return true } func checkpointDecl() agentllm.ToolDecl { return agentllm.ToolDecl{Name: workCheckpointTool, Description: `及时保存本次材料整理的阶段发现。只引用已提供的 sourceId/blockIndex 和连续原文短引;sourceIndex 是本条 findings.sources 的零起始索引。material_record 默认不附 evidence,由宿主按短引扩展整句并原样展示。唯一例外:单一来源、短引即完整原句且原句明确表达关系时,material_record 可附 direct_relation,恰 1 个前提 {"role":"direct_relation","sourceIndex":0},不写 value、不写 result;不满足则宿主转待确认。analysis/provisional/needs_user 的数字或分期归属只用 difference 或 unresolved_relation(不要用 direct_relation,宿主会把 analysis+direct_relation 转为 needs_user):difference 须有 total、phase 各一带 value,且 result 必须等于 total−phase;unresolved_relation 须有 total、phase、dependent 各一带 value,不写 result。允许模式示例:{"findingKey":"rec","text":"上线六月","kind":"material_record","sources":[{"sourceId":"s1","blockIndex":0,"quote":"上线六月"}]};{"findingKey":"rel","text":"第二期包含3项依赖外部接口的需求","kind":"material_record","sources":[{"sourceId":"s1","blockIndex":0,"quote":"第二期包含3项依赖外部接口的需求"}],"evidence":{"mode":"direct_relation","premises":[{"role":"direct_relation","sourceIndex":0}]}};{"findingKey":"diff","text":"若同一范围则差额4项","kind":"analysis","assumptions":"两处数字指同一需求范围","sources":[{"sourceId":"s1","blockIndex":0,"quote":"12项"},{"sourceId":"s2","blockIndex":0,"quote":"8项"}],"evidence":{"mode":"difference","premises":[{"role":"total","sourceIndex":0,"value":12},{"role":"phase","sourceIndex":1,"value":8}],"result":4}};{"findingKey":"gap","text":"接口依赖归属待确认","kind":"needs_user","sources":[{"sourceId":"s1","blockIndex":0,"quote":"12项"},{"sourceId":"s2","blockIndex":0,"quote":"8项"}],"evidence":{"mode":"unresolved_relation","premises":[{"role":"total","sourceIndex":0,"value":12},{"role":"phase","sourceIndex":1,"value":8},{"role":"dependent","sourceIndex":0,"value":3}]}}。evidence 结构不符(如 direct_relation 带 result)会整次检查点被拒,工具结果说明原因,须修正后重发;结构合规但宿主核不上的(前提与短引不符、direct_relation 条件不满足、原句语境不唯一、无 evidence 的非审慎分析、无 evidence 的 needs_user 且原文被替换)该条转为待确认并在工具结果说明。不得把推测写成已批准或确定归属。不额外调用摘要模型;每次发布1至3条新发现,最多6次;更正旧发现时用 corrects。本工具不创建确认按钮或暂停执行。`, Parameters: json.RawMessage(`{ "type":"object","additionalProperties":false,"required":["checkpointId","phaseId","findings"], "properties":{ "checkpointId":{"type":"string"}, "phaseId":{"type":"string","enum":["reading","crosscheck","summarizing","draft-ready"]}, "focus":{"type":"string"},"remaining":{"type":"string"},"nextStep":{"type":"string"}, "findings":{"type":"array","minItems":1,"maxItems":3,"items":{ "type":"object","additionalProperties":false,"required":["findingKey","text","kind","sources"], "properties":{ "findingKey":{"type":"string"},"text":{"type":"string"},"impact":{"type":"string"}, "kind":{"type":"string","enum":["material_record","analysis","provisional","needs_user"]}, "assumptions":{"type":"string"},"corrects":{"type":"string"}, "sources":{"type":"array","minItems":1,"items":{"type":"object","additionalProperties":false, "required":["sourceId","blockIndex","quote"],"properties":{ "sourceId":{"type":"string"},"blockIndex":{"type":"integer","minimum":0}, "quote":{"type":"string","description":"同一段落连续出现的原文短句;material_record 默认只靠此短引,宿主扩展为整句展示"}}}}, "evidence":{"type":"object","additionalProperties":false,"required":["mode","premises"], "description":"按 mode 的必填/禁填见工具说明;结构不符会整次被拒","properties":{ "mode":{"type":"string","enum":["direct_relation","difference","unresolved_relation"], "description":"material_record 仅在例外用 direct_relation;analysis/provisional/needs_user 只用 difference 或 unresolved_relation"}, "premises":{"type":"array","minItems":1,"maxItems":3, "description":"direct_relation 恰 1 个无 value;difference 为 total+phase 带 value;unresolved_relation 为 total+phase+dependent 带 value", "items":{"type":"object","additionalProperties":false, "required":["role","sourceIndex"],"properties":{ "role":{"type":"string","enum":["total","phase","dependent","direct_relation"], "description":"与 mode 匹配的前提角色,每种角色至多一次"}, "sourceIndex":{"type":"integer","minimum":0}, "value":{"type":"integer","minimum":0,"maximum":1000000, "description":"difference/unresolved_relation 的计数前提必填;direct_relation 禁止"}}}}, "result":{"type":"integer","minimum":0,"maximum":1000000, "description":"difference 必填且须等于 total−phase;direct_relation 与 unresolved_relation 禁止"}}} }}} } }`)} } func publishCheckpoint(rc RunCtx, args string) agenttools.Result { if !checkpointEnabled(rc) { return agenttools.Result{IsError: true, Content: "检查点发布不适用于当前任务或工具策略不允许"} } if len(args) > 16*1024 { return agenttools.Result{IsError: true, Content: "检查点超过16KiB;已有成果保留"} } var candidate core.WorkCheckpointCandidate d := json.NewDecoder(bytes.NewBufferString(args)) d.DisallowUnknownFields() if err := d.Decode(&candidate); err != nil { return agenttools.Result{IsError: true, Content: err.Error()} } var extra any if err := d.Decode(&extra); err != io.EOF { return agenttools.Result{IsError: true, Content: "检查点只接受一个JSON对象"} } if err := core.ValidateWorkCheckpoint(candidate); err != nil { return agenttools.Result{IsError: true, Content: err.Error()} } receipt, err := rc.(WorkCheckpointSink).PublishWorkCheckpoint(candidate) if err != nil { return agenttools.Result{IsError: true, Content: fmt.Sprintf("阶段发现未发布,已有成果保留:%v", err)} } content := "阶段发现已保存到当前任务卡。继续处理剩余工作;发布成功只表示来源片段通过校验,不表示分析结论已独立核实。" if receipt != "" { content += " " + receipt } return agenttools.Result{Content: content} }