Files
Agent-System-Core/task_snapshot.go
T
weichen 6e385743e4 Retain coverage limits in legacy task summaries
Why: Older clients must see the same coverage limits as structured task cards.

Evidence: Core suite and composition checks follow this projection update.

Impact: Projects optional limitations into existing list nodes.

Refs: fast-slow-u2-u3 Task 6
2026-10-13 02:08:00 +00:00

264 lines
9.8 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 core
import (
"encoding/json"
"fmt"
"strings"
"time"
)
type DocumentOrigin struct {
ArtifactID ID `json:"artifactId"`
ContentHash string `json:"contentHash"`
ParserRevision string `json:"parserRevision"`
}
type TaskPhase struct {
ID string `json:"id"`
Label string `json:"label"`
ObservedAt time.Time `json:"observedAt,omitempty"`
}
type TaskSourceRef struct {
ID string `json:"id"`
ArtifactID string `json:"artifactId"`
ContentHash string `json:"contentHash"`
ParserRevision string `json:"parserRevision"`
BlockIndex int `json:"blockIndex"`
Label string `json:"label"`
Quote string `json:"quote,omitempty"`
Availability string `json:"availability,omitempty"`
}
type TaskResultRef struct {
ArtifactID ID `json:"artifactId,omitempty"`
SurfaceID ID `json:"surfaceId,omitempty"`
Version string `json:"version,omitempty"`
ContentHash string `json:"contentHash,omitempty"`
Availability string `json:"availability"`
Name string `json:"name,omitempty"`
}
type TaskFinding struct {
ID string `json:"id"`
Text string `json:"text"`
Impact string `json:"impact,omitempty"`
Kind string `json:"kind"`
Status string `json:"status"`
Revision int64 `json:"revision"`
Sources []TaskSourceRef `json:"sources"`
Assumptions string `json:"assumptions,omitempty"`
Corrects string `json:"corrects,omitempty"`
}
type TaskSnapshot struct {
SchemaVersion int `json:"schemaVersion"`
TaskID ID `json:"taskId"`
AttemptID ID `json:"attemptId"`
CardID ID `json:"cardId"`
ProjectID ID `json:"projectId"`
ConversationID ID `json:"conversationId"`
SourceMessageID ID `json:"sourceMessageId,omitempty"`
ParentRunID ID `json:"parentRunId,omitempty"`
RunID ID `json:"runId,omitempty"`
OwnerInstanceID string `json:"ownerInstanceId"`
ExecutionEpoch int64 `json:"executionEpoch,omitempty"`
Revision int64 `json:"revision"`
SourceSeq int64 `json:"sourceSeq"`
StreamScope string `json:"streamScope"`
InputRevision string `json:"inputRevision,omitempty"`
Title string `json:"title"`
Goal string `json:"goal,omitempty"`
ExecutionState string `json:"executionState"`
OutcomeCompleteness string `json:"outcomeCompleteness"`
DeliveryState string `json:"deliveryState"`
Phase TaskPhase `json:"phase"`
Focus string `json:"focus,omitempty"`
Coverage string `json:"coverage,omitempty"`
Remaining string `json:"remaining,omitempty"`
NextStep string `json:"nextStep,omitempty"`
Conclusion string `json:"conclusion,omitempty"`
Findings []TaskFinding `json:"findings,omitempty"`
Limitations []string `json:"limitations,omitempty"`
Inputs []TaskSourceRef `json:"inputs,omitempty"`
Actions []UIAction `json:"actions,omitempty"`
ResultRefs []TaskResultRef `json:"resultRefs,omitempty"`
LastActivityAt time.Time `json:"lastActivityAt,omitempty"`
LastMeaningfulUpdateAt time.Time `json:"lastMeaningfulUpdateAt,omitempty"`
CheckpointKeys map[string]string `json:"checkpointKeys,omitempty"`
CheckpointCount int `json:"checkpointCount,omitempty"`
CheckpointSeq int64 `json:"checkpointSeq,omitempty"`
}
type TaskSourceQuote struct {
SourceID string `json:"sourceId"`
BlockIndex int `json:"blockIndex"`
Quote string `json:"quote"`
}
type WorkCheckpointCandidate struct {
CheckpointID string `json:"checkpointId"`
PhaseID string `json:"phaseId"`
Focus string `json:"focus,omitempty"`
Remaining string `json:"remaining,omitempty"`
NextStep string `json:"nextStep,omitempty"`
Findings []WorkFindingCandidate `json:"findings"`
}
type WorkFindingCandidate struct {
FindingKey string `json:"findingKey"`
Text string `json:"text"`
Impact string `json:"impact,omitempty"`
Kind string `json:"kind"`
Assumptions string `json:"assumptions,omitempty"`
Corrects string `json:"corrects,omitempty"`
Sources []TaskSourceQuote `json:"sources"`
}
// Validate enforces wire limits; owner/source authorization belongs to Backend.
func (s TaskSnapshot) Validate() error {
if s.SchemaVersion != 1 {
return fmt.Errorf("task.schema: unsupported")
}
if s.TaskID == "" || s.AttemptID == "" || s.CardID == "" || s.ProjectID == "" || s.ConversationID == "" || s.OwnerInstanceID == "" {
return fmt.Errorf("task.scope: missing identity")
}
if s.StreamScope != "conversation:"+s.ConversationID {
return fmt.Errorf("task.scope: inconsistent stream")
}
if s.Revision < 1 || s.Revision > 9007199254740991 || s.SourceSeq < 0 || s.SourceSeq > 9007199254740991 || s.ExecutionEpoch < 0 {
return fmt.Errorf("task.revision: unsafe cursor")
}
switch s.ExecutionState {
case "queued", "running", "awaiting_user", "completed", "failed", "interrupted":
default:
return fmt.Errorf("task.state: unknown")
}
if (s.ExecutionState == "running" || s.ExecutionState == "awaiting_user") && s.RunID == "" {
return fmt.Errorf("task.run: active task requires run")
}
switch s.OutcomeCompleteness {
case "none", "partial", "complete":
default:
return fmt.Errorf("task.outcome: unknown")
}
switch s.DeliveryState {
case "pending", "available", "reported":
default:
return fmt.Errorf("task.delivery: unknown")
}
active := 0
for _, f := range s.Findings {
if f.ID == "" || f.Revision < 1 {
return fmt.Errorf("task.finding: missing identity")
}
switch f.Kind {
case "material_record", "analysis", "provisional", "needs_user", "verified":
default:
return fmt.Errorf("task.finding.kind: unknown")
}
switch f.Status {
case "active":
active++
case "corrected", "withdrawn", "superseded":
default:
return fmt.Errorf("task.finding.status: unknown")
}
}
if active > 12 {
return fmt.Errorf("task.findings: too many active")
}
b, err := json.Marshal(s)
if err != nil {
return err
}
if len(b) > 64*1024 {
return fmt.Errorf("task.size: exceeds 64KiB")
}
return nil
}
func (s TaskSnapshot) Terminal() bool {
return s.ExecutionState == "completed" || s.ExecutionState == "failed" || s.ExecutionState == "interrupted"
}
func ValidateWorkCheckpoint(c WorkCheckpointCandidate) error {
if strings.TrimSpace(c.CheckpointID) == "" || len(c.CheckpointID) > 128 {
return fmt.Errorf("checkpoint.id: required or too long")
}
switch c.PhaseID {
case "reading", "crosscheck", "summarizing", "draft-ready":
default:
return fmt.Errorf("checkpoint.phase: unknown")
}
if len(c.Findings) == 0 || len(c.Findings) > 6 {
return fmt.Errorf("checkpoint.findings: require 1..6")
}
keys := map[string]bool{}
for _, f := range c.Findings {
switch f.Kind {
case "material_record", "analysis", "provisional", "needs_user":
default:
return fmt.Errorf("checkpoint.kind: model cannot grant verified status")
}
if f.FindingKey == "" || keys[f.FindingKey] || strings.TrimSpace(f.Text) == "" {
return fmt.Errorf("checkpoint.finding: empty or duplicate key")
}
keys[f.FindingKey] = true
if len(f.Sources) == 0 {
return fmt.Errorf("checkpoint.sources: evidence required")
}
for _, src := range f.Sources {
if src.SourceID == "" || src.BlockIndex < 0 || strings.TrimSpace(src.Quote) == "" {
return fmt.Errorf("checkpoint.source: invalid")
}
}
if (f.Kind == "analysis" || f.Kind == "provisional") && strings.TrimSpace(f.Assumptions) == "" {
return fmt.Errorf("checkpoint.assumptions: required")
}
}
b, err := json.Marshal(c)
if err != nil {
return err
}
if len(b) > 16*1024 {
return fmt.Errorf("checkpoint.size: exceeds 16KiB")
}
return nil
}
func (s TaskSnapshot) SummaryBody() []UINode {
labels := map[string]string{"queued": "排队中", "running": "进行中", "awaiting_user": "需要你处理", "completed": "已完成", "failed": "执行失败", "interrupted": "已中断"}
var lines []string
for _, f := range s.Findings {
if f.Status == "active" {
line := f.Text
if f.Impact != "" {
line += ";" + f.Impact
}
lines = append(lines, line)
}
}
note := strings.Join(lines, "\n")
if note == "" {
note = TaskSummaryNoFindingsYet
if s.ExecutionState == "queued" {
note = "尚未开始处理,等待执行资源"
}
if s.Terminal() {
note = s.Conclusion
if note == "" {
note = "没有可用阶段成果"
}
}
}
v := TaskSummaryView{Focus: s.Focus, FindingsNote: note, Remaining: s.Remaining, NextStep: s.NextStep, OutcomeLine: s.Conclusion}
var body []UINode
if s.Terminal() {
body = v.FinishedBody(labels[s.ExecutionState], nil)
} else {
body = v.RunningBody(labels[s.ExecutionState], nil)
}
if s.Coverage != "" {
body = append(body, UINode{Type: UIKV, KV: [][2]string{{"提供范围", s.Coverage}}})
}
if len(s.Limitations) > 0 {
body = append(body, UINode{Type: UIList, Title: "范围与限制", Items: s.Limitations})
}
for _, r := range s.ResultRefs {
if r.ArtifactID != "" && r.Availability == "available" {
body = append(body, UINode{Type: UIDocRef, DocID: r.ArtifactID, Title: r.Name})
}
}
return body
}