Files
Agent-System-Backend/internal/runmgr/material_retrieve.go
T
weichen 583c074aa4
ci / vet-and-test (pull_request) Successful in 50s
Close U3 review gaps before provider continuation
Why: Missing Usage must stop a paid sequence before a tool terminal is executable; exact attachment names and host-owned choices must also remain reliable.

Evidence: Each reported behavior had a failing regression before its fix. Full Backend vet/Go tests, live-tag budget tests, ten race replays, and six task-owned HTTP/Playwright scenarios passed with the aligned Agent-Native and Agent-Squad pins.

Impact: Evaluation terminal accounting becomes durable before delivery; material fallback matches bounded filenames; ready choices use host options without redundant model fields. No new model calls or budget authorization.

Refs: U3 r0.4 independent review Critical 1 and Important 3-4
2026-10-14 03:57:33 +00:00

188 lines
5.3 KiB
Go

package runmgr
import (
"crypto/sha256"
"encoding/hex"
"strings"
"unicode"
"unicode/utf8"
core "agent-system-core"
)
// materialRetrievalResult keeps ordinary retrieval authoritative. A no-hit
// material child may fall back only to the attachments frozen for this task.
func (c *runCtx) materialRetrievalResult(q core.RetrievalQuery, hits []core.RetrievalHit, err error) ([]core.RetrievalHit, error) {
if err != nil || len(hits) != 0 || c.subAgent != materialSubagentID {
return hits, err
}
return c.materialAttachmentFallback(q)
}
func (c *runCtx) materialAttachmentFallback(q core.RetrievalQuery) ([]core.RetrievalHit, error) {
if c.task.cardID == "" || c.run.Owner == "" || c.run.ProjectID == "" ||
q.ProjectID != c.run.ProjectID || strings.TrimSpace(q.Text) == "" {
return nil, nil
}
if len(q.Sources) > 0 {
allowed := false
for _, source := range q.Sources {
allowed = allowed || source == "upload"
}
if !allowed {
return nil, nil
}
}
var matched *core.ParsedDocument
for i := range c.attachments {
name := strings.TrimSpace(c.attachments[i].Meta.Filename)
if exactFilenameIndex(q.Text, name) < 0 {
continue
}
if matched != nil {
return nil, nil // duplicate or multiple filenames: never guess
}
matched = &c.attachments[i]
}
if matched == nil || matched.Meta.Origin == nil {
return nil, nil
}
origin := matched.Meta.Origin
if origin.ArtifactID == "" || origin.ContentHash == "" || origin.ParserRevision == "" {
return nil, nil
}
card, err := c.mgr.st.GetCard(c.run.Owner, c.task.cardID)
if err != nil {
return nil, err
}
s := card.Task
if s == nil || s.Terminal() || s.TaskID != c.task.taskID ||
s.AttemptID != c.task.attemptID || s.RunID != c.run.ID ||
s.OwnerInstanceID != c.task.instanceID || s.ExecutionEpoch != c.task.epoch {
return nil, nil
}
bound := 0
for _, input := range s.Inputs {
if input.ArtifactID == string(origin.ArtifactID) && input.ContentHash == origin.ContentHash &&
input.ParserRevision == origin.ParserRevision {
bound++
}
}
if bound != 1 {
return nil, nil
}
a, err := c.mgr.st.GetArtifact(c.run.Owner, string(origin.ArtifactID))
if err != nil {
return nil, err
}
if a.ProjectID != c.run.ProjectID || a.Name != matched.Meta.Filename {
return nil, nil
}
data, err := c.mgr.st.ArtifactContent(c.run.Owner, string(origin.ArtifactID))
if err != nil {
return nil, err
}
sum := sha256.Sum256(data)
if hex.EncodeToString(sum[:]) != origin.ContentHash {
return nil, nil
}
after, err := c.mgr.st.GetArtifact(c.run.Owner, string(origin.ArtifactID))
if err != nil || !after.UpdatedAt.Equal(a.UpdatedAt) {
return nil, err
}
full := strings.TrimSpace(matched.PlainText())
if full == "" {
return nil, nil
}
limit := c.prof.Context.MaxToolResultChars
if limit <= 0 {
limit = int(c.eff.GetNumber("context.maxToolResultChars", 20000))
}
if limit <= 0 || limit > 4000 {
limit = 4000
}
doc := core.KnowledgeDoc{Owner: c.run.Owner, ProjectID: c.run.ProjectID,
Source: "upload", Title: matched.Meta.Filename, Ref: string(origin.ArtifactID),
Chunk: materialExcerpt(full, q.Text, matched.Meta.Filename, c.attachments, origin.ArtifactID, c.eff, limit),
CreatedAt: a.CreatedAt}
if c.mgr.knowledgeResourceAllow != nil && !c.mgr.knowledgeResourceAllow(string(c.run.Owner), doc) {
return nil, nil
}
return []core.RetrievalHit{{Doc: doc, Score: 1, Kind: "fulltext"}}, nil
}
// A filename must stand as a whole query item, including when it has spaces.
// Nearby letters, digits, dots and hyphens may belong to another filename.
func exactFilenameIndex(query, name string) int {
if name == "" {
return -1
}
boundary := func(r rune) bool {
return unicode.IsSpace(r) || strings.ContainsRune(`"'“”‘’「」『』()()[]【】,,;;::`, r)
}
for from := 0; from < len(query); {
at := strings.Index(query[from:], name)
if at < 0 {
return -1
}
at += from
before := at == 0
if !before {
r, _ := utf8.DecodeLastRuneInString(query[:at])
before = boundary(r)
}
end := at + len(name)
after := end == len(query)
if !after {
r, _ := utf8.DecodeRuneInString(query[end:])
after = boundary(r)
}
if before && after {
return at
}
from = at + 1
}
return -1
}
func materialExcerpt(full, query, filename string, docs []core.ParsedDocument, id core.ID, eff core.EffectiveSettings, limit int) string {
runes := []rune(full)
start := 0
matchedTerm := false
withoutName := query
if at := exactFilenameIndex(query, filename); at >= 0 {
withoutName = query[:at] + query[at+len(filename):]
}
terms := strings.Fields(strings.TrimSpace(withoutName))
for _, term := range terms {
if at := strings.Index(full, term); at >= 0 {
matchedTerm = true
start = len([]rune(full[:at])) - 80
if start < 0 {
start = 0
}
break
}
}
if !matchedTerm {
perDoc := int(eff.GetNumber(materialPerDocKey, materialPerDocDef))
if perDoc <= 0 {
perDoc = materialPerDocDef
}
left := materialInlineTotal
for _, d := range docs {
count := len([]rune(strings.TrimSpace(d.PlainText())))
provided := min(perDoc, left, count)
if d.Meta.Origin != nil && d.Meta.Origin.ArtifactID == id {
if provided < len(runes) {
start = provided
}
break
}
left -= provided
}
}
end := min(start+limit, len(runes))
return string(runes[start:end])
}