Why: equal-length edits and parser changes must not reuse prior results. Evidence: content identity and provided-source tests RED to GREEN; backend suite recorded through evidence. Impact: trusted optional document origins, content keys and source catalog; no new routing or DB schema. Refs: U2/U3 Task2
129 lines
4.0 KiB
Go
129 lines
4.0 KiB
Go
package runmgr
|
|
|
|
import (
|
|
"agent-system-backend/internal/runtime"
|
|
core "agent-system-core"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
type materialInputItem struct {
|
|
ArtifactID core.ID
|
|
Parsed core.ParsedDocument
|
|
OriginalHash string
|
|
}
|
|
type materialSource struct {
|
|
Ref core.TaskSourceRef
|
|
Text string
|
|
Provided string
|
|
}
|
|
type materialSourceRegistry map[string]materialSource
|
|
|
|
func materialInputRevision(items []materialInputItem, processingRevision string) string {
|
|
docs := make([]core.ParsedDocument, 0, len(items))
|
|
for _, item := range items {
|
|
d := item.Parsed
|
|
origin := core.DocumentOrigin{}
|
|
if d.Meta.Origin != nil {
|
|
origin = *d.Meta.Origin
|
|
}
|
|
origin.ContentHash = item.OriginalHash
|
|
d.Meta.Origin = &origin
|
|
docs = append(docs, d)
|
|
}
|
|
sum := sha256.Sum256([]byte(materialKey(docs) + "\n" + processingRevision))
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
func materialSources(docs []core.ParsedDocument, perDoc int) materialSourceRegistry {
|
|
r := materialSourceRegistry{}
|
|
left := materialInlineTotal
|
|
if perDoc <= 0 {
|
|
perDoc = materialPerDocDef
|
|
}
|
|
for _, d := range docs {
|
|
cap := perDoc
|
|
if cap > left {
|
|
cap = left
|
|
}
|
|
text := strings.TrimSpace(d.PlainText())
|
|
rr := []rune(text)
|
|
if len(rr) < cap {
|
|
cap = len(rr)
|
|
}
|
|
provided := string(rr[:cap])
|
|
left -= cap
|
|
if d.Meta.Origin == nil || d.Meta.Origin.ArtifactID == "" || d.Meta.Origin.ContentHash == "" {
|
|
continue
|
|
}
|
|
for index, block := range d.Blocks {
|
|
full := strings.TrimSpace((core.ParsedDocument{Blocks: []core.DocBlock{block}}).PlainText())
|
|
if full == "" || !strings.Contains(provided, full) {
|
|
continue
|
|
}
|
|
sum := sha256.Sum256([]byte(fmt.Sprintf("%s:%s:%d", d.Meta.Origin.ArtifactID, d.Meta.Origin.ContentHash, index)))
|
|
id := "src_" + hex.EncodeToString(sum[:12])
|
|
r[id] = materialSource{Ref: core.TaskSourceRef{ID: id, ArtifactID: d.Meta.Origin.ArtifactID, ContentHash: d.Meta.Origin.ContentHash, ParserRevision: d.Meta.Origin.ParserRevision, BlockIndex: index, Label: fmt.Sprintf("%s · 段落%d", d.Meta.Filename, index+1), Availability: "available"}, Text: full, Provided: provided}
|
|
}
|
|
}
|
|
return r
|
|
}
|
|
func (r materialSourceRegistry) validate(q core.TaskSourceQuote) (core.TaskSourceRef, error) {
|
|
src, ok := r[q.SourceID]
|
|
if !ok || q.BlockIndex != src.Ref.BlockIndex || strings.TrimSpace(q.Quote) == "" || !strings.Contains(src.Text, q.Quote) || !strings.Contains(src.Provided, q.Quote) {
|
|
return core.TaskSourceRef{}, fmt.Errorf("checkpoint.source: quote not in provided block")
|
|
}
|
|
ref := src.Ref
|
|
ref.Quote = q.Quote
|
|
return ref, nil
|
|
}
|
|
func (r materialSourceRegistry) prompt() string {
|
|
keys := make([]string, 0, len(r))
|
|
for k := range r {
|
|
keys = append(keys, k)
|
|
}
|
|
sort.Strings(keys)
|
|
var b strings.Builder
|
|
if len(keys) > 0 {
|
|
b.WriteString("\n\n=== 可引用的已提供来源(不含未提供的全文) ===\n")
|
|
}
|
|
for _, id := range keys {
|
|
src := r[id]
|
|
fmt.Fprintf(&b, "sourceId=%s blockIndex=%d %s\n", id, src.Ref.BlockIndex, src.Ref.Label)
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func (c *runCtx) materialRevision() string {
|
|
perDoc := int(c.eff.GetNumber(materialPerDocKey, materialPerDocDef))
|
|
if perDoc <= 0 {
|
|
perDoc = materialPerDocDef
|
|
}
|
|
recipe := "material-recipe.v3"
|
|
if runner, ok := c.mgr.subRunner.(*runtime.SubLLMRunner); ok && runner.Prompts != nil {
|
|
if body, err := runner.Prompts.AgentPrompt(c.run.Owner, materialSubagentID); err == nil {
|
|
recipe += "\n" + body
|
|
}
|
|
if spec, ok := runner.Prompts.SubagentSpec(c.run.Owner, materialSubagentID); ok {
|
|
raw, _ := json.Marshal(spec)
|
|
recipe += "\n" + string(raw)
|
|
}
|
|
}
|
|
if perDoc == materialPerDocDef && recipe == "material-recipe.v3" {
|
|
return materialKey(c.attachments)
|
|
}
|
|
items := make([]materialInputItem, 0, len(c.attachments))
|
|
for _, d := range c.attachments {
|
|
item := materialInputItem{Parsed: d}
|
|
if d.Meta.Origin != nil {
|
|
item.ArtifactID = d.Meta.Origin.ArtifactID
|
|
item.OriginalHash = d.Meta.Origin.ContentHash
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
return materialInputRevision(items, fmt.Sprintf("%s\nperDoc=%d,total=%d", recipe, perDoc, materialInlineTotal))
|
|
}
|