Files
Agent-System-Backend/internal/runmgr/material_sources.go
T
weichen 5cdda85e18 Prioritize the first sourced material checkpoint
Why: the background model spent its first turns searching already provided text and failed before saving any staged finding.

Evidence: first-finding and two-phase RED→GREEN tests; SQLite and isolated PostgreSQL pass; full Backend vet and tests pass; v11 prompt grew 337 UTF-8 bytes in the fixed fixture.

Impact: material recipe v11 asks for one short sourced fact before optional search without changing the checkpoint schema or adding a separate model call.

Refs: U3 r0.4 design; internal/runmgr/material.go
2026-10-14 02:28:56 +00:00

166 lines
5.1 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
Start int // byte offsets in strings.TrimSpace(ParsedDocument.PlainText())
End int
}
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
}
cursor := 0
for index, block := range d.Blocks {
full := strings.TrimSpace((core.ParsedDocument{Blocks: []core.DocBlock{block}}).PlainText())
if full == "" {
continue
}
offset := strings.Index(text[cursor:], full)
if offset < 0 {
continue
}
start := cursor + offset
cursor = start + len(full)
if start >= len(provided) {
continue
}
end := cursor
if end > len(provided) {
end = len(provided)
}
providedBlock := text[start:end]
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: providedBlock, Start: start, End: end}
}
}
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 annotateProvidedMaterial(d core.ParsedDocument, provided string, sources materialSourceRegistry) string {
if provided == "" || d.Meta.Origin == nil {
return provided
}
type marker struct {
start int
id string
block int
}
markers := make([]marker, 0)
for id, src := range sources {
if src.Ref.ArtifactID != d.Meta.Origin.ArtifactID || src.Ref.ContentHash != d.Meta.Origin.ContentHash || src.Ref.ParserRevision != d.Meta.Origin.ParserRevision || src.Start < 0 || src.End > len(provided) || src.Start >= src.End {
continue
}
if provided[src.Start:src.End] != src.Provided {
continue
}
markers = append(markers, marker{start: src.Start, id: id, block: src.Ref.BlockIndex})
}
sort.Slice(markers, func(i, j int) bool {
if markers[i].start != markers[j].start {
return markers[i].start < markers[j].start
}
return markers[i].block < markers[j].block
})
var b strings.Builder
cursor := 0
for _, mark := range markers {
if mark.start < cursor {
continue
}
b.WriteString(provided[cursor:mark.start])
fmt.Fprintf(&b, "sourceId=%s blockIndex=%d\n", mark.id, mark.block)
cursor = mark.start
}
b.WriteString(provided[cursor:])
return b.String()
}
func (c *runCtx) materialRevision() string {
perDoc := int(c.eff.GetNumber(materialPerDocKey, materialPerDocDef))
if perDoc <= 0 {
perDoc = materialPerDocDef
}
recipe := "material-recipe.v11"
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)
}
}
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\nfocus=%s", recipe, perDoc, materialInlineTotal, messageText(c.userMessage)))
}