Files
Agent-System-Backend/internal/runmgr/task_checkpoint.go
T

206 lines
7.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package runmgr
import (
"agent-system-backend/internal/store"
core "agent-system-core"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"strings"
"time"
)
func (c *runCtx) PublishWorkCheckpoint(candidate core.WorkCheckpointCandidate) (string, error) {
if c.run.Kind != core.RunSubagent || c.subAgent != materialSubagentID || c.task.cardID == "" {
return "", fmt.Errorf("checkpoint.scope: material task required")
}
if !c.prof.Tools.ToolAllowed("publish_work_checkpoint") || c.prof.Tools.NeedsConfirm("publish_work_checkpoint") {
return "", fmt.Errorf("checkpoint.policy: denied")
}
if err := core.ValidateWorkCheckpoint(candidate); err != nil {
return "", err
}
raw, _ := json.Marshal(candidate)
sum := sha256.Sum256(raw)
digest := hex.EncodeToString(sum[:])
card, err := c.mgr.st.GetCard(c.run.Owner, c.task.cardID)
if err != nil {
return "", err
}
s := card.Task
if s == nil || s.Terminal() || s.TaskID != c.task.taskID || s.AttemptID != c.task.attemptID || s.RunID != c.run.ID || s.OwnerInstanceID != c.task.instanceID || s.ExecutionEpoch != c.task.epoch {
return "", store.ErrConflict
}
witnesses := map[string]store.ArtifactWitness{}
verifiedSources := make([][]core.TaskSourceRef, len(candidate.Findings))
for i, f := range candidate.Findings {
for _, q := range f.Sources {
src, err := c.sources.validate(q)
if err != nil {
return "", err
}
bound := false
for _, input := range s.Inputs {
if input.ArtifactID == src.ArtifactID && input.ContentHash == src.ContentHash && input.ParserRevision == src.ParserRevision {
bound = true
break
}
}
if !bound {
return "", fmt.Errorf("checkpoint.source: not in task input revision")
}
a, err := c.mgr.st.GetArtifact(c.run.Owner, src.ArtifactID)
if err != nil {
return "", fmt.Errorf("checkpoint.source: unavailable")
}
if !c.mgr.taskSourceAvailable(c.run.Owner, c.run.ProjectID, src) {
return "", fmt.Errorf("checkpoint.source: unavailable or changed")
}
after, err := c.mgr.st.GetArtifact(c.run.Owner, src.ArtifactID)
if err != nil || !after.UpdatedAt.Equal(a.UpdatedAt) {
return "", store.ErrConflict
}
witnesses[src.ArtifactID] = store.ArtifactWitness{ArtifactID: src.ArtifactID, UpdatedAt: a.UpdatedAt.Format(time.RFC3339Nano)}
verifiedSources[i] = append(verifiedSources[i], src)
}
}
normalized, notes, err := normalizeMaterialEvidence(candidate, c.sources)
if err != nil {
return "", err
}
if normalized.Focus != "" || normalized.Remaining != "" || normalized.NextStep != "" {
notes = append(notes, "任务状态按工作阶段显示中性说明")
}
switch candidate.PhaseID {
case "reading":
normalized.Focus, normalized.Remaining, normalized.NextStep = "整理材料中的事实", "仍需核对差异和缺项", "继续核对材料"
case "crosscheck":
normalized.Focus, normalized.Remaining, normalized.NextStep = "核对材料中的差异", "仍需整理结论", "结合依据形成结果"
case "summarizing":
normalized.Focus, normalized.Remaining, normalized.NextStep = "整理已核对的要点", "仍需形成完整答复", "完成整理结果"
case "draft-ready":
normalized.Focus, normalized.Remaining, normalized.NextStep = "形成整理结果", "等待完整答复", "输出最终结果"
}
if err := validateMaterialApprovalClaims(normalized); err != nil {
return "", err
}
for i, f := range normalized.Findings {
for j, q := range f.Sources {
src, err := c.sources.validate(q)
if err != nil || src.ID != verifiedSources[i][j].ID || src.ArtifactID != verifiedSources[i][j].ArtifactID {
return "", fmt.Errorf("checkpoint.source: normalized quote unavailable")
}
verifiedSources[i][j] = src
}
}
findings := make([]core.TaskFinding, 0, len(normalized.Findings))
critical := false
for i, f := range normalized.Findings {
next := core.TaskFinding{ID: canonicalFindingID(c.task.attemptID, f.FindingKey), Revision: 1, Text: f.Text, Impact: f.Impact, Kind: f.Kind, Status: "active", Assumptions: f.Assumptions, Sources: verifiedSources[i]}
if f.Corrects != "" {
critical = true
next.Corrects = canonicalFindingID(c.task.attemptID, f.Corrects)
}
if f.Kind == "material_record" {
// A quotation proves what the source says, not an unstated approval
// or model paraphrase. Keep interpretations in their own finding kinds.
quotes := make([]string, 0, len(next.Sources))
seen := map[string]bool{}
for _, src := range next.Sources {
line := src.Label + ":「" + src.Quote + "」"
if !seen[line] {
quotes = append(quotes, line)
seen[line] = true
}
}
next.Text = strings.Join(quotes, "\n")
next.Impact, next.Assumptions = "", ""
}
findings = append(findings, next)
}
if old, ok := s.CheckpointKeys[candidate.CheckpointID]; ok {
if old == digest {
return strings.Join(notes, ";"), nil
}
return "", fmt.Errorf("checkpoint.id: payload conflict")
}
if !critical && s.CheckpointCount >= 6 {
return "", fmt.Errorf("checkpoint.limit: six updates already saved; continue to final result")
}
next := *s
next.Findings = append([]core.TaskFinding(nil), s.Findings...)
next.CheckpointKeys = map[string]string{}
for k, v := range s.CheckpointKeys {
next.CheckpointKeys[k] = v
}
for _, f := range findings {
corrected := f.Corrects == ""
for i := range next.Findings {
old := &next.Findings[i]
if old.ID == f.ID && old.Revision >= f.Revision {
f.Revision = old.Revision + 1
}
if old.Status == "active" && (old.ID == f.ID || old.ID == f.Corrects) {
if f.Corrects != "" {
old.Status = "corrected"
corrected = true
} else {
old.Status = "superseded"
}
}
}
if !corrected {
return "", fmt.Errorf("checkpoint.corrects: active finding not found")
}
next.Findings = append(next.Findings, f)
}
next.CheckpointKeys[candidate.CheckpointID] = digest
next.CheckpointSeq++
if !critical {
next.CheckpointCount++
}
next.Revision++
next.OutcomeCompleteness = "partial"
next.DeliveryState = "available"
next.Focus = normalized.Focus
next.Remaining = normalized.Remaining
next.NextStep = normalized.NextStep
now := time.Now().UTC()
next.Phase = core.TaskPhase{ID: candidate.PhaseID, Label: map[string]string{"reading": "读取材料", "crosscheck": "核对材料", "summarizing": "整理结论", "draft-ready": "形成阶段成果"}[candidate.PhaseID], ObservedAt: now}
next.LastActivityAt = now
next.LastMeaningfulUpdateAt = now
if err = next.Validate(); err != nil {
if !critical {
return "", err
}
// Reject an oversized replacement, while immediately withdrawing the known-wrong claim.
next.Findings = next.Findings[:len(s.Findings)]
next.Focus = ""
next.Remaining = "更正说明超过展示上限,原结论已撤回,请核对来源"
next.NextStep = "结合原件重新核验"
next.Conclusion = "原结论已撤回,尚无可展示的替代结论"
next.Phase = core.TaskPhase{ID: "correction", Label: "原结论已撤回", ObservedAt: now}
for i := range next.Findings {
if next.Findings[i].Status == "corrected" {
next.Findings[i].Status = "withdrawn"
}
}
if err = next.Validate(); err != nil {
return "", err
}
}
proofs := make([]store.ArtifactWitness, 0, len(witnesses))
for _, w := range witnesses {
proofs = append(proofs, w)
}
if err := c.mgr.cards.CommitTaskSources(c.run.Owner, next, s.Revision, proofs); err != nil {
return "", err
}
return strings.Join(notes, ";"), nil
}
func (c *runCtx) WorkCheckpointAvailable() bool {
return c.task.cardID != "" && c.subAgent == materialSubagentID && len(c.sources) > 0
}