a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
134 lines
3.8 KiB
Go
134 lines
3.8 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
// creditFileConsumption is the tool-call observer: when the agent opens
|
|
// a file (get_editing_context / read_file) shortly after a search, every
|
|
// symbol in that file the search surfaced is credited to the search's
|
|
// query — the same implicit (query → symbol) signal get_symbol_source
|
|
// records, extended to the "I'm about to work here" file-open tools.
|
|
// Cheap-gated on a fresh search so a file open with no recent search
|
|
// pays nothing.
|
|
func (s *Server) creditFileConsumption(ctx context.Context, filePath string) {
|
|
if s == nil || s.combo == nil || filePath == "" {
|
|
return
|
|
}
|
|
sess := s.sessionFor(ctx)
|
|
if sess == nil || !sess.hasFreshSearch() {
|
|
return
|
|
}
|
|
nodes := s.readerFor(ctx).GetFileNodes(filePath)
|
|
if len(nodes) == 0 {
|
|
return
|
|
}
|
|
ids := make([]string, 0, len(nodes))
|
|
for _, n := range nodes {
|
|
if n != nil && n.ID != "" {
|
|
ids = append(ids, n.ID)
|
|
}
|
|
}
|
|
query, matched := sess.attributedConsumptionBatch(ids)
|
|
if query != "" && len(matched) > 0 {
|
|
s.combo.RecordBatch(query, matched)
|
|
}
|
|
}
|
|
|
|
// forceInjectCap bounds how many learned-but-unfetched symbols a single
|
|
// search may force-inject. A small cap nudges the result set toward
|
|
// symbols the agent has reached for before on this query without letting
|
|
// the learned channel flood out fresh BM25 hits.
|
|
const forceInjectCap = 3
|
|
|
|
// forceInjectLearnedCandidates surfaces symbols that the implicit-
|
|
// feedback loop associates with this query but that BM25 never fetched —
|
|
// the "force-inject bypass-RRF" path. Without it, a symbol the agent
|
|
// repeatedly picks for a query can never re-appear once it drops out of
|
|
// the BM25 candidate set; the learned boost only reorders what BM25
|
|
// already returned.
|
|
//
|
|
// Sources, in priority order: the exact-query combo index, the
|
|
// per-keyword combo index, and the task-scoped feedback "missing" list.
|
|
// Injected candidates are deduped against the existing pool, fetched
|
|
// from the graph, then run through the same post-filter the BM25 results
|
|
// passed (repo / kind / lang / path / corpus / session scope) so an
|
|
// injected symbol can never escape the caller's scope. They are appended
|
|
// before the rerank so the combo / feedback signals rank them in
|
|
// context rather than stapling them to the tail.
|
|
func (s *Server) forceInjectLearnedCandidates(ctx context.Context, query string, nodes []*graph.Node, postFilter func([]*graph.Node) []*graph.Node) []*graph.Node {
|
|
if s == nil || query == "" {
|
|
return nodes
|
|
}
|
|
seen := make(map[string]struct{}, len(nodes))
|
|
for _, n := range nodes {
|
|
if n != nil {
|
|
seen[n.ID] = struct{}{}
|
|
}
|
|
}
|
|
|
|
// Gather learned candidate IDs in priority order, strongest boost
|
|
// first within each source.
|
|
var ids []string
|
|
appendBoosted := func(m map[string]float64) {
|
|
if len(m) == 0 {
|
|
return
|
|
}
|
|
type kv struct {
|
|
id string
|
|
v float64
|
|
}
|
|
kvs := make([]kv, 0, len(m))
|
|
for id, v := range m {
|
|
kvs = append(kvs, kv{id, v})
|
|
}
|
|
sort.Slice(kvs, func(i, j int) bool {
|
|
if kvs[i].v != kvs[j].v {
|
|
return kvs[i].v > kvs[j].v
|
|
}
|
|
return kvs[i].id < kvs[j].id
|
|
})
|
|
for _, k := range kvs {
|
|
ids = append(ids, k.id)
|
|
}
|
|
}
|
|
if s.combo != nil {
|
|
appendBoosted(s.combo.BoostMap(query))
|
|
appendBoosted(s.combo.KeywordBoostMap(query))
|
|
}
|
|
if s.feedback != nil && s.feedback.HasData() {
|
|
ids = append(ids, s.feedback.MissedSymbolsForQuery(query, 2)...)
|
|
}
|
|
if len(ids) == 0 {
|
|
return nodes
|
|
}
|
|
|
|
reader := s.readerFor(ctx)
|
|
var fetched []*graph.Node
|
|
for _, id := range ids {
|
|
if len(fetched) >= forceInjectCap {
|
|
break
|
|
}
|
|
if _, dup := seen[id]; dup {
|
|
continue
|
|
}
|
|
seen[id] = struct{}{}
|
|
if n := reader.GetNode(id); n != nil {
|
|
fetched = append(fetched, n)
|
|
}
|
|
}
|
|
if len(fetched) == 0 {
|
|
return nodes
|
|
}
|
|
if postFilter != nil {
|
|
fetched = postFilter(fetched)
|
|
}
|
|
if len(fetched) == 0 {
|
|
return nodes
|
|
}
|
|
return append(nodes, fetched...)
|
|
}
|