diff --git a/agent/coverage_params_test.go b/agent/coverage_params_test.go new file mode 100644 index 0000000..1030aec --- /dev/null +++ b/agent/coverage_params_test.go @@ -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") + } +} diff --git a/agent/mainagent.go b/agent/mainagent.go index 6d4aaca..5f8016a 100644 --- a/agent/mainagent.go +++ b/agent/mainagent.go @@ -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() // 本任务的工作目录 /tasks/,先建好。 mainDir := ensureRunDir(m.workDir, taskID, 0) diff --git a/agent/tools.go b/agent/tools.go index e16b4da..53073d0 100644 --- a/agent/tools.go +++ b/agent/tools.go @@ -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 diff --git a/agent/tools_insert.go b/agent/tools_insert.go index dff7878..9d0eca4 100644 --- a/agent/tools_insert.go +++ b/agent/tools_insert.go @@ -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 diff --git a/agent/worker.go b/agent/worker.go index 3d00371..4965e3c 100644 --- a/agent/worker.go +++ b/agent/worker.go @@ -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 只留静态角色正文 diff --git a/server/orchestration.go b/server/orchestration.go index 0f52a9c..5ee78f0 100644 --- a/server/orchestration.go +++ b/server/orchestration.go @@ -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)