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
166 lines
5.1 KiB
Go
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)))
|
|
}
|