refactor(agent): 测试范围与资产覆盖度开关解耦,移除 insert_assets 的 related 入参

task_scope 是任务的授权边界、资产查询的过滤基准,覆盖度开关只该决定要不要
拿它当分母算指标,不该决定要不要累积范围本身。此前关闭覆盖度会让 auto 与
agent 两条写入路径同时失效,task_scope 只剩界面手工添加的行。

- insert_assets 的自动入范围(source='auto')无条件执行,不再判 coverageDisabled
- add_task_scope 移出 coverageOnlyTools,planner / 任务内主 Agent / 目标拆解
  Agent 在覆盖度关闭时照样持有;list_untested_assets 仍随开关隐藏(纯覆盖度视角)
- 移除 insert_assets 每条资产的 related 入参:唯一作用是决定要不要入范围,值又
  不落库(同一资产再次登记即作废、界面上也看不出谁被判为无关),连带删除
  StripCoverageParams 及其专用的 deepCopyJSONMap / nestedMap
- add_task_scope 与 goalsScopeTail 的描述改为「授权边界」优先、「覆盖度分母」其次
This commit is contained in:
Autumn-27 committed 2026-09-11 13:33:44 -04:00
1 parent 33d3d00c4b
commit 42f0ff97d3
7 files changed
+22 -124

No files matched your search

+10 -58
View File
@@ -180,9 +180,11 @@ func NewToolSet(ts *db.ExplorationStore, worker string) *ToolSet {
func (t *ToolSet) SetTaskID(id int64) { t.taskID = id }
// SetCoverageEnabled records whether this task has the asset-coverage feature on
// (default enabled). Passing false makes graphOverviewData omit the coverage block,
// the insertAssets auto-scope hook a no-op, and CoverageTools reports the two
// coverage-only tools so callers can drop them from the agent's tool list.
// (default enabled). Passing false makes graphOverviewData omit the coverage block
// and DropCoverageTools filter the two coverage-only tools out of the agent's tool
// list. It does NOT stop scope accumulation: insertAssets' auto-scope hook runs
// either way, because task_scope is the task's range boundary (the filter basis for
// asset queries), not merely a coverage denominator.
func (t *ToolSet) SetCoverageEnabled(enabled bool) { t.coverageDisabled = !enabled }
// CoverageDisabled reports whether the coverage feature is off for this task.
@@ -191,7 +193,11 @@ func (t *ToolSet) CoverageDisabled() bool { return t.coverageDisabled }
// coverageOnlyTools are the LLM tools that only make sense when asset coverage is
// on. When the feature is off they are filtered out of the agent's tool list so
// they neither pollute the prompt nor let the model build a disabled denominator.
var coverageOnlyTools = map[string]bool{"add_task_scope": true, "list_untested_assets": true}
// add_task_scope is deliberately NOT here: task_scope is the task's range boundary
// (the filter basis for asset queries), not merely a coverage denominator, so the
// agents that own范围定义 keep it either way — in lockstep with insertAssets'
// auto-scope hook, which also runs regardless of the switch.
var coverageOnlyTools = map[string]bool{"list_untested_assets": true}
// DropCoverageTools returns tools with the coverage-only ones removed when this
// task has the feature disabled; otherwise it returns tools unchanged.
@@ -209,60 +215,6 @@ func (t *ToolSet) DropCoverageTools(tools []actool.CoreTool) []actool.CoreTool {
return out
}
// StripCoverageParams hides coverage-only parameters (currently insert_assets'
// per-item `related`, which only decides whether an asset enters the coverage
// denominator) from the model-facing schema when coverage is disabled — the param
// has no effect then, so showing it just pollutes the prompt. MUST run on the FINAL
// tool list (after AugmentTools/ToolResolve): the DB tools table is authoritative on
// schema, so stripping the code schema earlier would be overwritten. No-op when
// enabled or when the list has no insert_assets. The schema is deep-copied before
// editing so the shared/DB schema map is never mutated.
func (t *ToolSet) StripCoverageParams(tools []actool.CoreTool) []actool.CoreTool {
if !t.coverageDisabled {
return tools
}
for i, tool := range tools {
if tool.Name() != "insert_assets" {
continue
}
schema := deepCopyJSONMap(tool.InputSchema())
if props, ok := nestedMap(schema, "properties", "assets", "items", "properties"); ok {
delete(props, "related")
}
tools[i] = DecorateTool(tool, tool.Description(), schema)
}
return tools
}
// deepCopyJSONMap returns a JSON round-trip deep copy of a schema map so callers can
// edit it without touching the original (which may be shared/cached). Falls back to
// the input on any marshal error (edits then become best-effort no-ops upstream).
func deepCopyJSONMap(m map[string]any) map[string]any {
b, err := json.Marshal(m)
if err != nil {
return m
}
var out map[string]any
if json.Unmarshal(b, &out) != nil {
return m
}
return out
}
// nestedMap walks a chain of string keys through nested map[string]any values,
// returning the final map and whether the whole path resolved to one.
func nestedMap(m map[string]any, keys ...string) (map[string]any, bool) {
cur := m
for _, k := range keys {
next, ok := cur[k].(map[string]any)
if !ok {
return nil, false
}
cur = next
}
return cur, true
}
// Cross-task reuse: exported accessors returning the per-task tool logic bound to
// THIS ToolSet's store. Host-side orchestration tools build a ToolSet for an
// arbitrary task, then Call these — so cross-task reads/hint reuse the exact