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"` Evidence *WorkFindingEvidence `json:"evidence,omitempty"` } // WorkFindingEvidence is a model-supplied candidate, not a persisted proof. // Backend checks the quoted source text and computes any resulting statement. type WorkEvidencePremise struct { Role string `json:"role"` SourceIndex int `json:"sourceIndex"` Value *int `json:"value,omitempty"` } type WorkFindingEvidence struct { Mode string `json:"mode"` Premises []WorkEvidencePremise `json:"premises"` Result *int `json:"result,omitempty"` } func validateWorkFindingEvidence(e *WorkFindingEvidence, sourceCount int) error { if e == nil { return nil } var required []string switch e.Mode { case "direct_relation": required = []string{"direct_relation"} if e.Result != nil { return fmt.Errorf("checkpoint.evidence: direct relation cannot have result") } case "difference": required = []string{"total", "phase"} if e.Result == nil || *e.Result < 0 || *e.Result > 1000000 { return fmt.Errorf("checkpoint.evidence: difference requires bounded result") } case "unresolved_relation": required = []string{"total", "phase", "dependent"} if e.Result != nil { return fmt.Errorf("checkpoint.evidence: unresolved relation cannot have result") } default: return fmt.Errorf("checkpoint.evidence: unknown mode") } if len(e.Premises) != len(required) { return fmt.Errorf("checkpoint.evidence: wrong premise count") } roles := make(map[string]bool, len(required)) for _, premise := range e.Premises { if roles[premise.Role] || premise.SourceIndex < 0 || premise.SourceIndex >= sourceCount { return fmt.Errorf("checkpoint.evidence: duplicate role or invalid source index") } roles[premise.Role] = true if e.Mode == "direct_relation" { if premise.Value != nil { return fmt.Errorf("checkpoint.evidence: direct relation cannot have value") } } else if premise.Value == nil || *premise.Value < 0 || *premise.Value > 1000000 { return fmt.Errorf("checkpoint.evidence: count premise requires bounded value") } } for _, role := range required { if !roles[role] { return fmt.Errorf("checkpoint.evidence: missing premise role") } } return nil } // 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 err := validateWorkFindingEvidence(f.Evidence, len(f.Sources)); err != nil { return err } 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 }