Files
ARTEX/agent/tools_digest.go
T
BaaRai e233c02314 Translate all Chinese text to English
Comments, doc comments, UI labels, log and error messages, LLM prompt
strings, markdown docs, test data and assertions. Identifiers, enum
keys, routes, placeholders and code structure are unchanged.

Notes:
- docs/漏洞流量证据.md renamed to docs/vulnerability-traffic-evidence.md
- tests that exercised CJK specific handling (GBK zip names, Chinese
  full text search) now use ASCII equivalents; go build passes and the
  test suite matches the pre translation baseline (remaining failures
  need a PostgreSQL DSN and fail identically on the original)
- frontend mention syntax now uses English kinds (finding, asset, ...)
  matching the backend, chat mentions tests pass
2026-10-08 23:14:34 +02:00

166 lines
5.8 KiB
Go

package agent
// cold-digest §6: graph_overview folding + the restore tools.
//
// coldDigestsRecent — builds the folded cold region for graph_overview:
// cold_digests (flat {id, body, member_count}), newest-member first, capped.
// expand_digest(id) — level-1 restore: a digest's member compact list.
import (
"context"
"encoding/json"
"fmt"
"sort"
"github.com/Autumn-27/artex/db"
actool "github.com/Autumn-27/norma/tool"
)
// digestMemberEntry builds the compact per-member view expand_digest returns —
// same shape as recent_facts / recent_done_intents (§6.1 middle level). store is
// the digest's OWNING store (the current task, or a read-only source task §2).
func (t *ToolSet) digestMemberEntry(store *db.ExplorationStore, id int64) map[string]any {
n, _ := store.GetNode(id)
if n == nil {
return map[string]any{"id": id, "missing": true}
}
m := compactNode(n)
m["state"] = n.State
var p map[string]any
if json.Unmarshal(n.Payload, &p) == nil {
if c, ok := p["confidence"].(string); ok && c != "" {
m["confidence"] = c
}
}
return m
}
// coldDigestsRecent returns a store's active digests as flat bodies for
// graph_overview, ordered by the recency of their freshest member (max member id ≈
// latest cooled node — a digest near the live frontier is likelier relevant), and
// capped at `cap`. Overflow digest ids are returned separately (moreIDs) so they
// stay reachable via expand_digest even when not shown inline — cold_digests is the
// only exit for folded cold nodes. Shared by the current task overview and the
// read-only related-task overview (§2 cross-task reuse).
func coldDigestsRecent(store *db.ExplorationStore, cap int) (shown []map[string]any, moreIDs []int64) {
ads, err := store.ActiveDigests()
if err != nil || len(ads) == 0 {
return nil, nil
}
type dg struct {
id int64
entry map[string]any
freshness int64 // max member id (ids are monotonic ≈ creation time)
}
items := make([]dg, 0, len(ads))
for _, d := range ads {
var p struct {
Body string `json:"body"`
}
_ = json.Unmarshal(d.Payload, &p)
ms, _ := store.DigestMembers(d.ID) // sorted asc → last = freshest
var fresh int64
if len(ms) > 0 {
fresh = ms[len(ms)-1]
}
items = append(items, dg{
id: d.ID,
entry: map[string]any{"id": d.ID, "body": p.Body, "member_count": len(ms)},
freshness: fresh,
})
}
sort.Slice(items, func(i, j int) bool { return items[i].freshness > items[j].freshness })
for i, it := range items {
if i < cap {
shown = append(shown, it.entry)
} else {
moreIDs = append(moreIDs, it.id)
}
}
return shown, moreIDs
}
// hiddenMembersFor returns a predicate telling whether a member is hidden (folded
// into an active digest AND still cold) in the given store — so a source task's
// overview folds exactly the way that task folds itself (§2 cross-task: "whatever
// display logic the current task uses, the related task uses the same"). A revived
// (now hot) covered member is NOT hidden (§6 render-time revival check). Returns a
// never-hidden predicate when the store has no digests.
func hiddenMembersFor(store *db.ExplorationStore) func(int64) bool {
covered, err := store.CoveredMembers()
if err != nil || len(covered) == 0 {
return func(int64) bool { return false }
}
var hot map[int64]bool
if cg, _, err := loadColdGraph(store); err == nil {
hot = cg.hotSet()
}
return func(id int64) bool { _, c := covered[id]; return c && !hot[id] }
}
// resolveDigest finds a digest node by id in the current task, else in a direct
// source task (read-only, §2). Returns the node, its owning store, and the source
// task id (0 = current task).
func (t *ToolSet) resolveDigest(id int64) (*db.Node, *db.ExplorationStore, int64) {
if n, _ := t.ts.GetNode(id); n != nil && n.Kind == db.KindDigest {
return n, t.ts, 0
}
srcs, _ := t.ts.DirectSourceStores()
for _, s := range srcs {
if n, _ := s.Store.GetNode(id); n != nil && n.Kind == db.KindDigest {
return n, s.Store, s.Task.TaskID
}
}
return nil, nil, 0
}
// expandDigest returns a digest's covered members as a compact list (§6.1). It is
// a distinct tool from node_detail because it returns a LIST of members, not one
// node's full detail.
func (t *ToolSet) expandDigest() actool.CoreTool {
return t.writeExpTool("expand_digest",
"Expand a cold digest: returns the compact list of its folded members (id/summary/state/confidence), same shape as the overview's recent_facts/recent_done_intents. For a member's full detail/evidence use node_detail(member_id).",
map[string]any{
"type": "object",
"properties": map[string]any{
"id": map[string]any{"type": "integer", "description": "digest node id (from the overview's cold_digests)"},
},
"required": []any{"id"},
},
func(ctx context.Context, raw json.RawMessage) (actool.Result, error) {
var in struct {
ID int64 `json:"id"`
}
_ = json.Unmarshal(raw, &in)
n, store, srcTaskID := t.resolveDigest(in.ID)
if n == nil {
return jsonResult(map[string]any{"error": fmt.Sprintf("#%d is not a digest node (not found in this task or in directly related tasks)", in.ID)})
}
var p struct {
Body string `json:"body"`
}
_ = json.Unmarshal(n.Payload, &p)
members, _ := store.DigestMembers(in.ID)
list := make([]map[string]any, 0, len(members))
for _, m := range members {
entry := t.digestMemberEntry(store, m)
if srcTaskID > 0 { // related task's members: read-only, carry the inherited marker (§2)
entry["inherited"] = true
entry["source_task_id"] = srcTaskID
}
list = append(list, entry)
}
out := map[string]any{
"id": in.ID,
"state": n.State, // active / superseded
"body": p.Body,
"members": list,
}
if srcTaskID > 0 {
out["inherited"] = true
out["source_task_id"] = srcTaskID
}
return jsonResult(out)
})
}