feat(insert_assets): 资产可标记是否与当前任务相关(related)
insert_assets 每条资产新增 related 入参(默认 true):仅在任务开启资产 覆盖度功能时生效——true 才自动纳入覆盖度(测试范围分母),false 则只入 共享资产库、不计入本任务覆盖度(如顺带发现的旁站/无关资产)。 - Call 内部按 related + coverageDisabled 双重 gate 决定是否 AddAutoScope - 覆盖度关闭时 related 无意义,经 StripCoverageParams 从最终工具 schema 剔除(必须在 ToolResolve 的 DB schema 覆盖之后做,故在装配链末端处理) - 旧库经 refreshBuiltinToolSchemas(flag v6) 刷新 insert_assets schema, 否则首插入only 的 seed 收不到新参数 - 补 StripCoverageParams 回归测试(开启保留/关闭隐藏/不改原始 schema) Claude-Session: https://claude.ai/code/session_01Jm87Gr5fq5aso6SfLyguhx
This commit is contained in:
1 parent
eb3f278711
commit
22ae280584
6 files changed
+126
-6
No files matched your search
@@ -0,0 +1,46 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
actool "github.com/Autumn-27/norma/tool"
|
||||
)
|
||||
|
||||
// hasRelatedParam reports whether insert_assets' schema still advertises the
|
||||
// per-item `related` parameter (properties.assets.items.properties.related).
|
||||
func hasRelatedParam(tool actool.CoreTool) bool {
|
||||
props, ok := nestedMap(tool.InputSchema(), "properties", "assets", "items", "properties")
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
_, ok = props["related"]
|
||||
return ok
|
||||
}
|
||||
|
||||
// TestStripCoverageParams verifies the insert_assets `related` param is visible
|
||||
// while coverage is on and hidden once it's off, and that stripping never mutates
|
||||
// the original tool's schema (deep copy).
|
||||
func TestStripCoverageParams(t *testing.T) {
|
||||
ts := NewToolSet(nil, "")
|
||||
insert := ts.insertAssets()
|
||||
if !hasRelatedParam(insert) {
|
||||
t.Fatal("insert_assets 应默认带 related 参数")
|
||||
}
|
||||
|
||||
// Enabled (default): schema unchanged, related still present.
|
||||
ts.SetCoverageEnabled(true)
|
||||
if got := ts.StripCoverageParams([]actool.CoreTool{insert}); !hasRelatedParam(got[0]) {
|
||||
t.Error("覆盖度开启时不应剔除 related 参数")
|
||||
}
|
||||
|
||||
// Disabled: related stripped from the model-facing schema.
|
||||
ts.SetCoverageEnabled(false)
|
||||
out := ts.StripCoverageParams([]actool.CoreTool{insert})
|
||||
if hasRelatedParam(out[0]) {
|
||||
t.Error("覆盖度关闭时应隐藏 related 参数")
|
||||
}
|
||||
// The original tool must be untouched (deep copy, no shared-map mutation).
|
||||
if !hasRelatedParam(insert) {
|
||||
t.Error("StripCoverageParams 不应修改原始 tool 的 schema")
|
||||
}
|
||||
}
|
||||
@@ -89,6 +89,7 @@ func (m *MainAgent) Chat(ctx context.Context, taskID int64, as *db.AssetStore, t
|
||||
base := append(tsx.DropCoverageTools(tsx.MainAgentTools()), actool.DefaultTools()...)
|
||||
ctx = WithRunInfo(ctx, RunInfo{TaskID: taskID, ExplorationID: explorationID(ts)})
|
||||
tools, def, cleanup := AugmentTools(ctx, "mainagent", base)
|
||||
tools = tsx.StripCoverageParams(tools) // 覆盖度关闭时隐藏 insert_assets 的 related 入参
|
||||
defer cleanup()
|
||||
// 本任务的工作目录 <workDir>/tasks/<taskID>,先建好。
|
||||
mainDir := ensureRunDir(m.workDir, taskID, 0)
|
||||
|
||||
@@ -186,6 +186,60 @@ 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
|
||||
|
||||
+16
-2
@@ -62,6 +62,13 @@ type assetInputItem struct {
|
||||
// ---- endpoint ----
|
||||
Method string `json:"method"`
|
||||
Params []map[string]any `json:"params"`
|
||||
|
||||
// Related marks whether this asset is relevant to the CURRENT test task and
|
||||
// should therefore auto-enter the coverage system (task_scope denominator).
|
||||
// nil = default true (relevant → auto-scoped). false = insert into the shared
|
||||
// asset库 but keep it OUT of this task's coverage. Only consulted when the task
|
||||
// has the coverage feature enabled; ignored (no auto-scope either way) when off.
|
||||
Related *bool `json:"related"`
|
||||
}
|
||||
|
||||
// insertAssets is the unified insert_assets agent tool.
|
||||
@@ -150,6 +157,11 @@ func (t *ToolSet) insertAssets() actool.CoreTool {
|
||||
"description": "请求参数列表,每条含 location(query/body/header/path)/name/value/type(可选,追加不覆盖)",
|
||||
"items": map[string]any{"type": "object"},
|
||||
},
|
||||
// 覆盖度相关:该资产是否与当前测试任务有关。
|
||||
"related": map[string]any{
|
||||
"type": "boolean",
|
||||
"description": "该资产是否与【当前测试任务】相关:true(默认)才自动纳入资产覆盖度(测试范围分母);false 则只入库、不计入本任务覆盖度(如顺带发现的旁站/无关资产)。仅在任务开启资产覆盖度功能时生效。",
|
||||
},
|
||||
}, "type"),
|
||||
},
|
||||
}, "assets"),
|
||||
@@ -274,8 +286,10 @@ func (t *ToolSet) insertAssets() actool.CoreTool {
|
||||
t.anchorOwner(id)
|
||||
// 自动入测试范围(source='auto'):只对 worker 顶层显式插入的这一项,按其
|
||||
// 类型加保守范围;side-effect 派生的资产不经此处,故范围不盲目扩大。taskID=0 时无操作。
|
||||
// 资产覆盖度功能关闭时不再累积测试范围(分母)。
|
||||
if !t.coverageDisabled {
|
||||
// 资产覆盖度功能关闭时不再累积测试范围(分母);related=false(与当前任务无关)
|
||||
// 的资产也只入库、不计入覆盖度。related 省略/null 视为 true(默认纳入)。
|
||||
related := item.Related == nil || *item.Related
|
||||
if !t.coverageDisabled && related {
|
||||
svcIP := item.ServiceIP
|
||||
if svcIP == "" {
|
||||
svcIP = item.IP
|
||||
|
||||
@@ -269,6 +269,7 @@ func (w *Worker) Execute(ctx context.Context, name string, taskID int64, as *db.
|
||||
base = append(base, actool.DefaultTools()...)
|
||||
ctx = WithRunInfo(ctx, RunInfo{TaskID: taskID, ExplorationID: explorationID(ts), IntentID: intent.ID})
|
||||
tools, def, cleanup := AugmentTools(ctx, "worker", base)
|
||||
tools = tsx.StripCoverageParams(tools) // 覆盖度关闭时隐藏 insert_assets 的 related 入参
|
||||
defer cleanup()
|
||||
|
||||
// 意图 + 全局态势改放【启动 user 消息】(见下方 input),system 只留静态角色正文
|
||||
|
||||
@@ -462,7 +462,7 @@ func (s *Server) seedOrchestrationTools() {
|
||||
// reaches an old DB otherwise. Preserves each tool's agent binding + enabled flag.
|
||||
// Bump the flag whenever these tools' schemas/descriptions change in code.
|
||||
func (s *Server) refreshBuiltinToolSchemas() {
|
||||
const flag = "tool_schema_refresh_v5_spawn_seed_intent"
|
||||
const flag = "tool_schema_refresh_v6_insert_assets_related"
|
||||
if v, _, _ := s.m.pg.GetSetting(flag); v == "true" {
|
||||
return
|
||||
}
|
||||
@@ -473,10 +473,14 @@ func (s *Server) refreshBuiltinToolSchemas() {
|
||||
log.Printf("[tools] refresh %s schema failed: %v", t.Name(), err)
|
||||
}
|
||||
}
|
||||
// 同时把 planner 的 goal_met 描述刷成代码默认:旧库 seed 的描述带“结束本轮规划”的
|
||||
// 误导,会让 planner 把 goal_met 当成“结束空轮”的手段、刚开跑就误判整个任务完成。
|
||||
// 同时把部分内置 agent 工具刷成代码默认:
|
||||
// - goal_met:旧库 seed 的描述带“结束本轮规划”的误导,会让 planner 把它当成
|
||||
// “结束空轮”的手段、刚开跑就误判整个任务完成。
|
||||
// - insert_assets:新增 related 入参(标记资产是否与当前任务相关、决定是否入覆盖度),
|
||||
// SeedTool 首插入only,旧库已 seed 的 schema 否则收不到这个新参数。
|
||||
refreshBuiltin := map[string]bool{"goal_met": true, "insert_assets": true}
|
||||
for _, sd := range agent.BuiltinToolSeeds() {
|
||||
if sd.Key != "goal_met" {
|
||||
if !refreshBuiltin[sd.Key] {
|
||||
continue
|
||||
}
|
||||
schema, _ := json.Marshal(sd.Schema)
|
||||
|
||||
Reference in new issue
Block a user