diff --git a/agent/blackboard_inheritance_test.go b/agent/blackboard_inheritance_test.go index 4549ff5..9f55359 100644 --- a/agent/blackboard_inheritance_test.go +++ b/agent/blackboard_inheritance_test.go @@ -103,9 +103,8 @@ func TestGraphOverviewExpandsAssociatedCompanyScope(t *testing.T) { if !ok || len(keywords) != 1 || keywords[0] != keyword { t.Fatalf("company keywords missing: %#v", scopeRows[0]["company_keywords"]) } - hosts, ok := coverage["hosts"].([]string) - if !ok || !containsString(hosts, domain) { - t.Fatalf("company asset host missing from agent context: %#v", coverage["hosts"]) + if hc, _ := coverage["host_count"].(int); hc < 1 { + t.Fatalf("company asset host not counted in agent context: %#v", coverage["host_count"]) } untested := callReadJSON(t, tools.listUntestedAssets(), `{"type":"root_domain","page":1,"page_size":10}`) if !strings.Contains(fmt.Sprint(untested), domain) { @@ -153,9 +152,8 @@ func TestGraphOverviewExpandsAssociatedCompanyScope(t *testing.T) { if !ok { t.Fatalf("coverage-disabled task lost asset context: %#v", disabledOverview["coverage"]) } - disabledHosts, ok := disabledCoverage["hosts"].([]string) - if !ok || !containsString(disabledHosts, domain) { - t.Fatalf("coverage-disabled task lost company asset host: %#v", disabledCoverage["hosts"]) + if hc, _ := disabledCoverage["host_count"].(int); hc < 1 { + t.Fatalf("coverage-disabled task lost company asset host count: %#v", disabledCoverage["host_count"]) } if _, exists := disabledCoverage["denominator"]; exists { t.Fatalf("coverage-disabled task unexpectedly exposed metrics: %#v", disabledCoverage) @@ -176,15 +174,6 @@ func TestGraphOverviewExpandsAssociatedCompanyScope(t *testing.T) { } } -func containsString(items []string, target string) bool { - for _, item := range items { - if item == target { - return true - } - } - return false -} - func companiesName(t *testing.T, companies *db.CompanyStore, companyID int64) string { t.Helper() company, err := companies.GetCompany(companyID) @@ -321,9 +310,8 @@ func TestBlackboardToolsReadDirectSources(t *testing.T) { if !ok { t.Fatalf("coverage missing from overview: %#v", overview["coverage"]) } - hosts, ok := coverage["hosts"].([]string) - if !ok || len(hosts) != 1 || hosts[0] != host || coverage["host_count"] != 1 { - t.Fatalf("inherited host context missing: %#v", coverage) + if coverage["host_count"] != 1 { + t.Fatalf("inherited host context missing (want host_count=1): %#v", coverage) } facts := callReadJSON(t, tools.listFacts(), `{}`).(map[string]any)["facts"].([]any) diff --git a/agent/tools.go b/agent/tools.go index c6894aa..2f1f0e5 100644 --- a/agent/tools.go +++ b/agent/tools.go @@ -412,6 +412,12 @@ func (t *ToolSet) graphOverviewData() map[string]any { } out["running_intents"] = compactIntents(running, parentsOf, yieldsOf) out["recent_done_intents"] = compactIntents(recentDone, parentsOf, yieldsOf) + // done_intents_total:已结束意图(done/blocked/exhausted)总数,与 recent_done_intents + // 平行命名——后者只是它的最新窗口(≤15)截断视图。两键并排即自描述:"看到的是 N/总数", + // 让 planner 去重时别把"没显示"当成"没派过",无需在提示词里另行解释。 + if dt, err := t.ts.CountFinishedIntents(); err == nil { + out["done_intents_total"] = dt + } out["frontier_open"] = len(fr) // findings (confirmed vulns) and facts (worker exploration results) are // now distinct node kinds. recent_facts surfaces fact summaries (esp. @@ -532,16 +538,9 @@ func (t *ToolSet) graphOverviewData() map[string]any { m["scope"] = scope } if hosts, err := t.as.HostsByTaskWithSources(t.taskID); err == nil { - const hostContextLimit = 500 - visible := hosts - if len(visible) > hostContextLimit { - visible = visible[:hostContextLimit] - } - m["hosts"] = visible + // 只给主机总数,不再把 host 列表平铺进 graph_overview(大范围任务里那是每轮 + // 都重复携带的大量字符串,对规划决策价值有限);具体主机按需 list_assets 查。 m["host_count"] = len(hosts) - if len(visible) < len(hosts) { - m["hosts_truncated"] = true - } } if len(m) > 0 { out["coverage"] = m diff --git a/db/exploration.go b/db/exploration.go index eb0e677..a4260c2 100644 --- a/db/exploration.go +++ b/db/exploration.go @@ -706,6 +706,18 @@ func (s *ExplorationStore) countByKindFiltered(kind, q string) (int, error) { return n, err } +// CountFinishedIntents counts this exploration's intents in a terminal explored +// state (done/blocked/exhausted) — graph_overview's done_intents_total, so the planner +// knows recent_done_intents (capped at a recent window) is a truncated view and +// stays cautious about "already tried" dedup. Excludes 'stopped' (killed/deleted), +// matching exactly what recent_done_intents surfaces. +func (s *ExplorationStore) CountFinishedIntents() (int, error) { + var n int + err := s.db.QueryRow(`SELECT COUNT(*) FROM exploration_nodes +WHERE exploration_id=$1 AND kind='intent' AND state IN ('done','blocked','exhausted')`, s.expID).Scan(&n) + return n, err +} + // GetNode returns one node of this exploration by id (nil, nil if not found). func (s *ExplorationStore) GetNode(id int64) (*Node, error) { n, err := scanNode(s.db.QueryRow(`SELECT `+nodeCols+` FROM exploration_nodes WHERE id=$1 AND exploration_id=$2`, id, s.expID))