agentcheckup 0.4.3__tar.gz → 0.4.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/CHANGELOG.md +3 -2
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/PKG-INFO +26 -2
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/README.md +25 -1
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/README.zh-CN.md +19 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/pyproject.toml +1 -1
- agentcheckup-0.4.4/scripts/benchmark.py +101 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/report.py +1 -1
- agentcheckup-0.4.4/tests/fixtures/collision_benchmark.json +177 -0
- agentcheckup-0.4.4/tests/test_benchmark.py +82 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/.gitattributes +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/.github/ISSUE_TEMPLATE/rule_request.yml +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/.github/pull_request_template.md +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/.github/workflows/action-selftest.yml +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/.github/workflows/ci.yml +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/.github/workflows/publish.yml +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/.gitignore +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/CODE_OF_CONDUCT.md +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/CONTRIBUTING.md +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/LICENSE +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/Makefile +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/SECURITY.md +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/action.yml +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/conftest.py +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/docs/github-action.md +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/docs/rules.md +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/scripts/bootstrap.ps1 +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/scripts/bootstrap.sh +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/__init__.py +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/__main__.py +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/cli.py +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/conflicts.py +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/discovery.py +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/engine.py +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/loader.py +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/models.py +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/rules/__init__.py +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/rules/base.py +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/rules/config_rules.py +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/rules/docs.py +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/rules/injection.py +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/rules/naming.py +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/rules/safety.py +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/rules/schema.py +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/src/agentcheckup/tokens.py +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/tests/fixtures/bad_tools.json +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/tests/fixtures/ci_config.json +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/tests/fixtures/claude_desktop_config.json +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/tests/fixtures/good_tools.json +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/tests/fixtures/mock_server.py +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/tests/test_cli.py +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/tests/test_conflicts.py +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/tests/test_discovery.py +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/tests/test_registry.py +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/tests/test_rules.py +0 -0
- {agentcheckup-0.4.3 → agentcheckup-0.4.4}/tests/test_scoring.py +0 -0
|
@@ -91,8 +91,9 @@ trusts, so the scoring and the report both changed.
|
|
|
91
91
|
### Notes
|
|
92
92
|
|
|
93
93
|
- Conflict detection produced **zero false positives** across the 18 real tools
|
|
94
|
-
from the three official servers.
|
|
95
|
-
|
|
94
|
+
from the three official servers. *(Corrected in 0.4.4: that measurement was
|
|
95
|
+
vacuous — those 18 tools contained no conflicts, so there was nothing to get
|
|
96
|
+
wrong. A real benchmark replaced it.)*
|
|
96
97
|
|
|
97
98
|
## [0.3.0] - 2026-11-07
|
|
98
99
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: agentcheckup
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.4
|
|
4
4
|
Summary: Diagnose your agent setup: tool conflicts, risky config, context budget.
|
|
5
5
|
Project-URL: Homepage, https://github.com/jiaqinq/agentcheckup
|
|
6
6
|
Project-URL: Issues, https://github.com/jiaqinq/agentcheckup/issues
|
|
@@ -149,7 +149,7 @@ The fastest way to keep a manifest from regressing, and the version you pin is
|
|
|
149
149
|
the version you run — the action installs from its own checkout.
|
|
150
150
|
|
|
151
151
|
```yaml
|
|
152
|
-
- uses: jiaqinq/agentcheckup@v0.4.
|
|
152
|
+
- uses: jiaqinq/agentcheckup@v0.4.4
|
|
153
153
|
with:
|
|
154
154
|
config: .mcp.json
|
|
155
155
|
fail-on: warning
|
|
@@ -205,6 +205,30 @@ false-positive rate made the report untrustworthy, which is worse than no report
|
|
|
205
205
|
|
|
206
206
|
It is a heuristic, and the docs say so. `AGT203` is a warning, not a verdict.
|
|
207
207
|
|
|
208
|
+
## How well does conflict detection work?
|
|
209
|
+
|
|
210
|
+
Measured, not asserted. `tests/fixtures/collision_benchmark.json` holds 44
|
|
211
|
+
hand-labelled tool pairs — 20 that should be flagged, 24 that should not — and
|
|
212
|
+
`scripts/benchmark.py` scores the detector against them:
|
|
213
|
+
|
|
214
|
+
| | value |
|
|
215
|
+
|---|---|
|
|
216
|
+
| pairs | 44 (20 positive, 24 negative) |
|
|
217
|
+
| precision | **0.952** |
|
|
218
|
+
| recall | **1.000** |
|
|
219
|
+
| F1 | 0.976 |
|
|
220
|
+
|
|
221
|
+
The one disagreement is a pair like `lookup_contact` / `lookup_user` with
|
|
222
|
+
near-identical descriptions: the detector flags it, the label says it should not.
|
|
223
|
+
That is arguably the detector being right and the label being strict.
|
|
224
|
+
|
|
225
|
+
Two honest caveats. The labels are the author's, not users' — the criterion was
|
|
226
|
+
"would a model picking the wrong one get a materially different result". And this
|
|
227
|
+
benchmark did not exist before 0.4.4; the earlier claim of "zero false positives"
|
|
228
|
+
came from running on 18 tools that contained no conflicts at all, which measured
|
|
229
|
+
nothing. `tests/test_benchmark.py` fails the build if precision or recall drops
|
|
230
|
+
below 0.90.
|
|
231
|
+
|
|
208
232
|
## How the score works
|
|
209
233
|
|
|
210
234
|
Most linters fail here: a rule that fires on 18 of 18 tools is a *preference*,
|
|
@@ -121,7 +121,7 @@ The fastest way to keep a manifest from regressing, and the version you pin is
|
|
|
121
121
|
the version you run — the action installs from its own checkout.
|
|
122
122
|
|
|
123
123
|
```yaml
|
|
124
|
-
- uses: jiaqinq/agentcheckup@v0.4.
|
|
124
|
+
- uses: jiaqinq/agentcheckup@v0.4.4
|
|
125
125
|
with:
|
|
126
126
|
config: .mcp.json
|
|
127
127
|
fail-on: warning
|
|
@@ -177,6 +177,30 @@ false-positive rate made the report untrustworthy, which is worse than no report
|
|
|
177
177
|
|
|
178
178
|
It is a heuristic, and the docs say so. `AGT203` is a warning, not a verdict.
|
|
179
179
|
|
|
180
|
+
## How well does conflict detection work?
|
|
181
|
+
|
|
182
|
+
Measured, not asserted. `tests/fixtures/collision_benchmark.json` holds 44
|
|
183
|
+
hand-labelled tool pairs — 20 that should be flagged, 24 that should not — and
|
|
184
|
+
`scripts/benchmark.py` scores the detector against them:
|
|
185
|
+
|
|
186
|
+
| | value |
|
|
187
|
+
|---|---|
|
|
188
|
+
| pairs | 44 (20 positive, 24 negative) |
|
|
189
|
+
| precision | **0.952** |
|
|
190
|
+
| recall | **1.000** |
|
|
191
|
+
| F1 | 0.976 |
|
|
192
|
+
|
|
193
|
+
The one disagreement is a pair like `lookup_contact` / `lookup_user` with
|
|
194
|
+
near-identical descriptions: the detector flags it, the label says it should not.
|
|
195
|
+
That is arguably the detector being right and the label being strict.
|
|
196
|
+
|
|
197
|
+
Two honest caveats. The labels are the author's, not users' — the criterion was
|
|
198
|
+
"would a model picking the wrong one get a materially different result". And this
|
|
199
|
+
benchmark did not exist before 0.4.4; the earlier claim of "zero false positives"
|
|
200
|
+
came from running on 18 tools that contained no conflicts at all, which measured
|
|
201
|
+
nothing. `tests/test_benchmark.py` fails the build if precision or recall drops
|
|
202
|
+
below 0.90.
|
|
203
|
+
|
|
180
204
|
## How the score works
|
|
181
205
|
|
|
182
206
|
Most linters fail here: a rule that fires on 18 of 18 tools is a *preference*,
|
|
@@ -128,6 +128,25 @@ agentcheckup rules # 列出全部规则
|
|
|
128
128
|
|
|
129
129
|
这是启发式,文档里写明了。`AGT203` 是警告,不是判决。
|
|
130
130
|
|
|
131
|
+
## 冲突检测到底准不准
|
|
132
|
+
|
|
133
|
+
这是测出来的,不是声称的。`tests/fixtures/collision_benchmark.json` 里有 44 对人工标注的工具对
|
|
134
|
+
(20 对该报、24 对不该报),`scripts/benchmark.py` 拿检测器去跑:
|
|
135
|
+
|
|
136
|
+
| 指标 | 值 |
|
|
137
|
+
|---|---|
|
|
138
|
+
| 样本 | 44 对(20 正 / 24 负) |
|
|
139
|
+
| 精确率 | **0.952** |
|
|
140
|
+
| 召回率 | **1.000** |
|
|
141
|
+
| F1 | 0.976 |
|
|
142
|
+
|
|
143
|
+
唯一的分歧是一对类似 `lookup_contact` / `lookup_user`、描述几乎一样的工具——检测器报了,
|
|
144
|
+
我的标注说不该报。**这更像是检测器对、标注太严。**
|
|
145
|
+
|
|
146
|
+
两个必须说明的限制:标注是我自己做的,不是用户标注(判断标准是「模型选错会不会拿到实质不同的结果」);
|
|
147
|
+
这个基准是 0.4.4 才有的,**之前那句「零误报」是在 18 个根本没有冲突的工具上跑出来的,什么都没测到**。
|
|
148
|
+
`tests/test_benchmark.py` 会在精确率或召回率跌破 0.90 时让 CI 失败。
|
|
149
|
+
|
|
131
150
|
## 分数是怎么算的
|
|
132
151
|
|
|
133
152
|
大多数 linter 在这里翻车:一条在 18 个工具上全部触发的规则,是**偏好**不是缺陷,
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Measure conflict detection against a labelled set of tool pairs.
|
|
2
|
+
|
|
3
|
+
The README used to claim "zero false positives" on the strength of three official
|
|
4
|
+
servers that contained no conflicts at all. That is not a measurement. This is:
|
|
5
|
+
every pair below was labelled by hand, so precision and recall can be computed
|
|
6
|
+
and regressions become visible.
|
|
7
|
+
|
|
8
|
+
python scripts/benchmark.py
|
|
9
|
+
|
|
10
|
+
Labels are the author's, not users'. The criterion applied to each pair:
|
|
11
|
+
"would a maintainer reasonably consider these two tools confusable by a model
|
|
12
|
+
choosing from one flat list?" A pair is positive when a user picking the wrong
|
|
13
|
+
one would get a materially different result.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
23
|
+
sys.path.insert(0, str(ROOT / "src"))
|
|
24
|
+
|
|
25
|
+
from agentcheckup.conflicts import ToolRef, detect_conflicts # noqa: E402
|
|
26
|
+
from agentcheckup.models import Tool # noqa: E402
|
|
27
|
+
|
|
28
|
+
FIXTURE = ROOT / "tests" / "fixtures" / "collision_benchmark.json"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def to_ref(side: dict) -> ToolRef:
|
|
32
|
+
tool = Tool.from_raw(
|
|
33
|
+
{
|
|
34
|
+
"name": side["name"],
|
|
35
|
+
"description": side["description"],
|
|
36
|
+
"inputSchema": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"properties": {"path": {"type": "string", "pattern": "^.*$"}},
|
|
39
|
+
"additionalProperties": False,
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
return ToolRef(server=side["server"], tool=tool)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def main() -> int:
|
|
47
|
+
cases = json.loads(FIXTURE.read_text(encoding="utf-8"))
|
|
48
|
+
|
|
49
|
+
tp = fp = tn = fn = 0
|
|
50
|
+
false_positives: list[str] = []
|
|
51
|
+
false_negatives: list[str] = []
|
|
52
|
+
|
|
53
|
+
for case in cases:
|
|
54
|
+
found = detect_conflicts([to_ref(case["a"]), to_ref(case["b"])])
|
|
55
|
+
predicted = bool(found)
|
|
56
|
+
actual = case["conflict"]
|
|
57
|
+
|
|
58
|
+
if predicted and actual:
|
|
59
|
+
tp += 1
|
|
60
|
+
elif predicted and not actual:
|
|
61
|
+
fp += 1
|
|
62
|
+
rules = ",".join(sorted({c.rule for c in found}))
|
|
63
|
+
false_positives.append(f" {case['id']} {rules} {case['why']}")
|
|
64
|
+
elif not predicted and actual:
|
|
65
|
+
fn += 1
|
|
66
|
+
false_negatives.append(f" {case['id']} missed {case['why']}")
|
|
67
|
+
else:
|
|
68
|
+
tn += 1
|
|
69
|
+
|
|
70
|
+
total = tp + fp + tn + fn
|
|
71
|
+
precision = tp / (tp + fp) if tp + fp else 0.0
|
|
72
|
+
recall = tp / (tp + fn) if tp + fn else 0.0
|
|
73
|
+
f1 = 2 * precision * recall / (precision + recall) if precision + recall else 0.0
|
|
74
|
+
accuracy = (tp + tn) / total if total else 0.0
|
|
75
|
+
|
|
76
|
+
print(f"pairs {total}")
|
|
77
|
+
print(f"positives {tp + fn}")
|
|
78
|
+
print(f"negatives {tn + fp}")
|
|
79
|
+
print()
|
|
80
|
+
print(f"true pos {tp}")
|
|
81
|
+
print(f"false pos {fp}")
|
|
82
|
+
print(f"true neg {tn}")
|
|
83
|
+
print(f"false neg {fn}")
|
|
84
|
+
print()
|
|
85
|
+
print(f"precision {precision:.3f}")
|
|
86
|
+
print(f"recall {recall:.3f}")
|
|
87
|
+
print(f"f1 {f1:.3f}")
|
|
88
|
+
print(f"accuracy {accuracy:.3f}")
|
|
89
|
+
|
|
90
|
+
if false_positives:
|
|
91
|
+
print("\nfalse positives")
|
|
92
|
+
print("\n".join(false_positives))
|
|
93
|
+
if false_negatives:
|
|
94
|
+
print("\nfalse negatives")
|
|
95
|
+
print("\n".join(false_negatives))
|
|
96
|
+
|
|
97
|
+
return 0
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
[
|
|
2
|
+
{"id": "p01", "conflict": true, "why": "同名工具,跨 server",
|
|
3
|
+
"a": {"server": "files", "name": "read_file", "description": "Read a text file from the sandbox and return its contents."},
|
|
4
|
+
"b": {"server": "git", "name": "read_file", "description": "Read a file as it exists in a git revision."}},
|
|
5
|
+
|
|
6
|
+
{"id": "p02", "conflict": true, "why": "同名,仅大小写不同,检测器会先 lower()",
|
|
7
|
+
"a": {"server": "files", "name": "read_file", "description": "Read a file from disk."},
|
|
8
|
+
"b": {"server": "s3", "name": "Read_File", "description": "Read an object from a bucket."}},
|
|
9
|
+
|
|
10
|
+
{"id": "p03", "conflict": true, "why": "单复数差异,归一化后同名",
|
|
11
|
+
"a": {"server": "files", "name": "read_files", "description": "Read several files and return their contents."},
|
|
12
|
+
"b": {"server": "git", "name": "read_file", "description": "Read one file from a revision."}},
|
|
13
|
+
|
|
14
|
+
{"id": "p04", "conflict": true, "why": "描述几乎一致,名字共享 search",
|
|
15
|
+
"a": {"server": "github", "name": "search_public", "description": "Search public repositories by keyword and return matching repository names."},
|
|
16
|
+
"b": {"server": "docs", "name": "search_private", "description": "Search private repositories by keyword and return matching repository names."}},
|
|
17
|
+
|
|
18
|
+
{"id": "p05", "conflict": true, "why": "同义动词 + 同一宾语,名字共享 file",
|
|
19
|
+
"a": {"server": "files", "name": "get_file", "description": "Fetch a file by path and return its bytes."},
|
|
20
|
+
"b": {"server": "docs", "name": "get_files", "description": "Fetch files by path and return their bytes."}},
|
|
21
|
+
|
|
22
|
+
{"id": "p06", "conflict": true, "why": "中文近义,描述几乎相同",
|
|
23
|
+
"a": {"server": "文件服务", "name": "读取文件", "description": "读取指定路径的文件内容并返回文本"},
|
|
24
|
+
"b": {"server": "文档服务", "name": "读取文档", "description": "读取指定路径的文件内容并返回字符串"}},
|
|
25
|
+
|
|
26
|
+
{"id": "p07", "conflict": true, "why": "同名删除工具",
|
|
27
|
+
"a": {"server": "files", "name": "delete_file", "description": "Delete a file at the given path."},
|
|
28
|
+
"b": {"server": "storage", "name": "delete_file", "description": "Delete an object from the bucket."}},
|
|
29
|
+
|
|
30
|
+
{"id": "p08", "conflict": true, "why": "单复数 + 同一目录语义",
|
|
31
|
+
"a": {"server": "files", "name": "list_dir", "description": "List entries in one directory under the sandbox root."},
|
|
32
|
+
"b": {"server": "shell", "name": "list_dirs", "description": "List entries in one directory under the sandbox root."}},
|
|
33
|
+
|
|
34
|
+
{"id": "p09", "conflict": true, "why": "同一动作的 issue 创建,描述高度重叠",
|
|
35
|
+
"a": {"server": "github", "name": "create_issue", "description": "Create an issue with a title, body and assignee in a repository."},
|
|
36
|
+
"b": {"server": "jira", "name": "create_issues", "description": "Create an issue with a title, body and assignee in a project."}},
|
|
37
|
+
|
|
38
|
+
{"id": "p10", "conflict": true, "why": "描述相同,名字只差一个词且共享 run",
|
|
39
|
+
"a": {"server": "shell", "name": "run_command", "description": "Run a shell command and return stdout and stderr."},
|
|
40
|
+
"b": {"server": "docker", "name": "run_commands", "description": "Run a shell command and return stdout and stderr."}},
|
|
41
|
+
|
|
42
|
+
{"id": "p11", "conflict": true, "why": "同名,一个本地一个远端",
|
|
43
|
+
"a": {"server": "files", "name": "write_file", "description": "Write text to a file on disk."},
|
|
44
|
+
"b": {"server": "s3", "name": "write_file", "description": "Write text to an object in a bucket."}},
|
|
45
|
+
|
|
46
|
+
{"id": "p12", "conflict": true, "why": "描述几乎逐字相同",
|
|
47
|
+
"a": {"server": "db", "name": "query_table", "description": "Query rows from a table using a SQL where clause and return them as JSON."},
|
|
48
|
+
"b": {"server": "warehouse", "name": "query_tables", "description": "Query rows from a table using a SQL where clause and return them as JSON."}},
|
|
49
|
+
|
|
50
|
+
{"id": "p13", "conflict": true, "why": "同一份数据的两种读取入口,名字共享 read",
|
|
51
|
+
"a": {"server": "cache", "name": "read_cache", "description": "Read a value from the cache by key."},
|
|
52
|
+
"b": {"server": "redis", "name": "read_cached", "description": "Read a value from the cache by key."}},
|
|
53
|
+
|
|
54
|
+
{"id": "p14", "conflict": true, "why": "中文同名",
|
|
55
|
+
"a": {"server": "文件服务", "name": "搜索文件", "description": "按关键词搜索文件并返回匹配的路径"},
|
|
56
|
+
"b": {"server": "代码服务", "name": "搜索文件", "description": "按关键词搜索仓库中的文件"}},
|
|
57
|
+
|
|
58
|
+
{"id": "p15", "conflict": true, "why": "单复数 + 同一搜索动作",
|
|
59
|
+
"a": {"server": "docs", "name": "search_doc", "description": "Search documents by title across the workspace and return matching ids."},
|
|
60
|
+
"b": {"server": "wiki", "name": "search_docs", "description": "Search documents by title across the workspace and return matching ids."}},
|
|
61
|
+
|
|
62
|
+
{"id": "p16", "conflict": true, "why": "同名上传",
|
|
63
|
+
"a": {"server": "s3", "name": "upload", "description": "Upload a local file to remote storage."},
|
|
64
|
+
"b": {"server": "gcs", "name": "upload", "description": "Upload a local file to remote storage."}},
|
|
65
|
+
|
|
66
|
+
{"id": "p17", "conflict": true, "why": "描述一致,名字共享 get 与 issue",
|
|
67
|
+
"a": {"server": "github", "name": "get_issue", "description": "Get one issue by number and return its title, body, state and labels."},
|
|
68
|
+
"b": {"server": "jira", "name": "get_issues", "description": "Get one issue by number and return its title, body, state and labels."}},
|
|
69
|
+
|
|
70
|
+
{"id": "p18", "conflict": true, "why": "同一动作的中英文混写,描述相同",
|
|
71
|
+
"a": {"server": "files", "name": "read_file", "description": "读取文件内容并返回文本"},
|
|
72
|
+
"b": {"server": "docs", "name": "read_file", "description": "读取文件内容并返回文本"}},
|
|
73
|
+
|
|
74
|
+
{"id": "p19", "conflict": true, "why": "单复数 + 同一删除动作,描述相同",
|
|
75
|
+
"a": {"server": "db", "name": "drop_table", "description": "Drop a table and all of its rows."},
|
|
76
|
+
"b": {"server": "warehouse", "name": "drop_tables", "description": "Drop a table and all of its rows."}},
|
|
77
|
+
|
|
78
|
+
{"id": "p20", "conflict": true, "why": "同名创建",
|
|
79
|
+
"a": {"server": "k8s", "name": "create_namespace", "description": "Create a namespace in the cluster."},
|
|
80
|
+
"b": {"server": "cloud", "name": "create_namespace", "description": "Create a namespace in the project."}},
|
|
81
|
+
|
|
82
|
+
{"id": "n01", "conflict": false, "why": "完全不同的事",
|
|
83
|
+
"a": {"server": "files", "name": "read_file", "description": "Read a text file from the sandbox and return its contents."},
|
|
84
|
+
"b": {"server": "jira", "name": "create_ticket", "description": "Create a ticket with a title, body and assignee."}},
|
|
85
|
+
|
|
86
|
+
{"id": "n02", "conflict": false, "why": "共享 search 但宾语不同,这是最常见的误报来源",
|
|
87
|
+
"a": {"server": "github", "name": "search_repositories", "description": "Search repositories by name across the org."},
|
|
88
|
+
"b": {"server": "docs", "name": "search_documents", "description": "Search documents by title across the workspace."}},
|
|
89
|
+
|
|
90
|
+
{"id": "n03", "conflict": false, "why": "共享 get 但对象完全不同",
|
|
91
|
+
"a": {"server": "time", "name": "get_current_time", "description": "Return the current time in a given timezone."},
|
|
92
|
+
"b": {"server": "weather", "name": "get_weather", "description": "Return the forecast for a city and date."}},
|
|
93
|
+
|
|
94
|
+
{"id": "n04", "conflict": false, "why": "共享 list 但宾语不同",
|
|
95
|
+
"a": {"server": "files", "name": "list_files", "description": "List files under a directory."},
|
|
96
|
+
"b": {"server": "db", "name": "list_tables", "description": "List tables in a database."}},
|
|
97
|
+
|
|
98
|
+
{"id": "n05", "conflict": false, "why": "共享 upload 但媒体类型不同",
|
|
99
|
+
"a": {"server": "assets", "name": "upload_image", "description": "Upload an image and return its asset id."},
|
|
100
|
+
"b": {"server": "docs", "name": "upload_document", "description": "Upload a document and return its document id."}},
|
|
101
|
+
|
|
102
|
+
{"id": "n06", "conflict": false, "why": "中文共享文件但动作相反",
|
|
103
|
+
"a": {"server": "文件服务", "name": "读取文件", "description": "读取指定路径的文件内容并返回文本"},
|
|
104
|
+
"b": {"server": "文件服务", "name": "删除文件", "description": "删除指定路径的文件"}},
|
|
105
|
+
|
|
106
|
+
{"id": "n07", "conflict": false, "why": "共享 file 但读与写相反",
|
|
107
|
+
"a": {"server": "files", "name": "read_file", "description": "Read a file and return its contents."},
|
|
108
|
+
"b": {"server": "files", "name": "write_file", "description": "Write text to a file."}},
|
|
109
|
+
|
|
110
|
+
{"id": "n08", "conflict": false, "why": "共享 git 但动作不同",
|
|
111
|
+
"a": {"server": "git", "name": "git_commit", "description": "Record staged changes as a commit."},
|
|
112
|
+
"b": {"server": "git", "name": "git_status", "description": "Show the working tree status."}},
|
|
113
|
+
|
|
114
|
+
{"id": "n09", "conflict": false, "why": "名字完全不共享词",
|
|
115
|
+
"a": {"server": "fetch", "name": "fetch", "description": "Fetch a URL and convert it to markdown."},
|
|
116
|
+
"b": {"server": "time", "name": "convert_time", "description": "Convert a time between timezones."}},
|
|
117
|
+
|
|
118
|
+
{"id": "n10", "conflict": false, "why": "共享 create 但资源不同",
|
|
119
|
+
"a": {"server": "github", "name": "create_branch", "description": "Create a git branch in a repository."},
|
|
120
|
+
"b": {"server": "jira", "name": "create_project", "description": "Create a project with a key and name."}},
|
|
121
|
+
|
|
122
|
+
{"id": "n11", "conflict": false, "why": "名字共享 file,但一个是读内容一个是读元数据",
|
|
123
|
+
"a": {"server": "files", "name": "read_file", "description": "Read the full text content of a file from disk."},
|
|
124
|
+
"b": {"server": "meta", "name": "file_info", "description": "Return size, mtime and permissions for a file."}},
|
|
125
|
+
|
|
126
|
+
{"id": "n12", "conflict": false, "why": "共享 search 但一个是代码一个是日志",
|
|
127
|
+
"a": {"server": "code", "name": "search_code", "description": "Search source files for a pattern."},
|
|
128
|
+
"b": {"server": "logs", "name": "search_logs", "description": "Search application logs for a pattern."}},
|
|
129
|
+
|
|
130
|
+
{"id": "n13", "conflict": false, "why": "共享 send 但渠道不同",
|
|
131
|
+
"a": {"server": "slack", "name": "send_message", "description": "Send a message to a channel."},
|
|
132
|
+
"b": {"server": "mail", "name": "send_email", "description": "Send an email to a recipient."}},
|
|
133
|
+
|
|
134
|
+
{"id": "n14", "conflict": false, "why": "中文共享仓库但动作不同",
|
|
135
|
+
"a": {"server": "代码服务", "name": "克隆仓库", "description": "把远端仓库克隆到本地目录"},
|
|
136
|
+
"b": {"server": "代码服务", "name": "删除仓库", "description": "删除远端仓库及其全部分支"}},
|
|
137
|
+
|
|
138
|
+
{"id": "n15", "conflict": false, "why": "共享 run 但一个是测试一个是迁移",
|
|
139
|
+
"a": {"server": "ci", "name": "run_tests", "description": "Run the test suite and return failures."},
|
|
140
|
+
"b": {"server": "db", "name": "run_migrations", "description": "Apply pending database migrations."}},
|
|
141
|
+
|
|
142
|
+
{"id": "n16", "conflict": false, "why": "共享 get 但一个是配置一个是统计",
|
|
143
|
+
"a": {"server": "config", "name": "get_config", "description": "Return the current configuration as JSON."},
|
|
144
|
+
"b": {"server": "metrics", "name": "get_stats", "description": "Return request counts for the last hour."}},
|
|
145
|
+
|
|
146
|
+
{"id": "n17", "conflict": false, "why": "描述用词相似但对象不同(跨域同义)",
|
|
147
|
+
"a": {"server": "crm", "name": "lookup_contact", "description": "Look up a contact by email and return the profile."},
|
|
148
|
+
"b": {"server": "auth", "name": "lookup_user", "description": "Look up a user by email and return the profile."}},
|
|
149
|
+
|
|
150
|
+
{"id": "n18", "conflict": false, "why": "共享 list 但一个是文件一个是容器",
|
|
151
|
+
"a": {"server": "docker", "name": "list_containers", "description": "List running containers."},
|
|
152
|
+
"b": {"server": "files", "name": "list_directory", "description": "List entries in a directory."}},
|
|
153
|
+
|
|
154
|
+
{"id": "n19", "conflict": false, "why": "共享 delete 但资源不同",
|
|
155
|
+
"a": {"server": "k8s", "name": "delete_pod", "description": "Delete a pod by name."},
|
|
156
|
+
"b": {"server": "s3", "name": "delete_object", "description": "Delete an object by key."}},
|
|
157
|
+
|
|
158
|
+
{"id": "n20", "conflict": false, "why": "名字共享 repo,一个是读一个是搜",
|
|
159
|
+
"a": {"server": "github", "name": "get_repo", "description": "Return metadata for one repository."},
|
|
160
|
+
"b": {"server": "github", "name": "search_repos", "description": "Search repositories by keyword."}},
|
|
161
|
+
|
|
162
|
+
{"id": "n21", "conflict": false, "why": "共享 write 但一个是文件一个是日志",
|
|
163
|
+
"a": {"server": "files", "name": "write_file", "description": "Write text to a file on disk."},
|
|
164
|
+
"b": {"server": "logs", "name": "write_log", "description": "Append a line to the application log."}},
|
|
165
|
+
|
|
166
|
+
{"id": "n22", "conflict": false, "why": "共享 create 但一个是目录一个是分支",
|
|
167
|
+
"a": {"server": "files", "name": "create_dir", "description": "Create a directory under the sandbox root."},
|
|
168
|
+
"b": {"server": "git", "name": "create_branch", "description": "Create a branch at the current HEAD."}},
|
|
169
|
+
|
|
170
|
+
{"id": "n23", "conflict": false, "why": "中文共享搜索但对象不同",
|
|
171
|
+
"a": {"server": "代码服务", "name": "搜索代码", "description": "在源码文件中搜索指定模式"},
|
|
172
|
+
"b": {"server": "日志服务", "name": "搜索日志", "description": "在应用日志中搜索指定模式"}},
|
|
173
|
+
|
|
174
|
+
{"id": "n24", "conflict": false, "why": "共享 read 但一个是文件一个是环境变量",
|
|
175
|
+
"a": {"server": "files", "name": "read_file", "description": "Read a file and return its contents."},
|
|
176
|
+
"b": {"server": "env", "name": "read_env", "description": "Read an environment variable by name."}}
|
|
177
|
+
]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Regression guard on the labelled collision benchmark.
|
|
2
|
+
|
|
3
|
+
`scripts/benchmark.py` prints the numbers for humans; this keeps them from
|
|
4
|
+
silently decaying. The floors are set a little below the measured values so a
|
|
5
|
+
small, deliberate trade-off does not fail the build, but a real regression does.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
15
|
+
sys.path.insert(0, str(ROOT / "src"))
|
|
16
|
+
|
|
17
|
+
from agentcheckup.conflicts import ToolRef, detect_conflicts # noqa: E402
|
|
18
|
+
from agentcheckup.models import Tool # noqa: E402
|
|
19
|
+
|
|
20
|
+
FIXTURE = Path(__file__).resolve().parent / "fixtures" / "collision_benchmark.json"
|
|
21
|
+
|
|
22
|
+
MIN_PRECISION = 0.90
|
|
23
|
+
MIN_RECALL = 0.90
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def to_ref(side: dict) -> ToolRef:
|
|
27
|
+
tool = Tool.from_raw(
|
|
28
|
+
{
|
|
29
|
+
"name": side["name"],
|
|
30
|
+
"description": side["description"],
|
|
31
|
+
"inputSchema": {
|
|
32
|
+
"type": "object",
|
|
33
|
+
"properties": {"path": {"type": "string", "pattern": "^.*$"}},
|
|
34
|
+
"additionalProperties": False,
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
)
|
|
38
|
+
return ToolRef(server=side["server"], tool=tool)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def score() -> tuple[float, float, list[str], list[str]]:
|
|
42
|
+
cases = json.loads(FIXTURE.read_text(encoding="utf-8"))
|
|
43
|
+
tp = fp = fn = 0
|
|
44
|
+
false_positives: list[str] = []
|
|
45
|
+
false_negatives: list[str] = []
|
|
46
|
+
|
|
47
|
+
for case in cases:
|
|
48
|
+
found = detect_conflicts([to_ref(case["a"]), to_ref(case["b"])])
|
|
49
|
+
predicted = bool(found)
|
|
50
|
+
if predicted and case["conflict"]:
|
|
51
|
+
tp += 1
|
|
52
|
+
elif predicted and not case["conflict"]:
|
|
53
|
+
fp += 1
|
|
54
|
+
false_positives.append(case["id"])
|
|
55
|
+
elif not predicted and case["conflict"]:
|
|
56
|
+
fn += 1
|
|
57
|
+
false_negatives.append(case["id"])
|
|
58
|
+
|
|
59
|
+
precision = tp / (tp + fp) if tp + fp else 0.0
|
|
60
|
+
recall = tp / (tp + fn) if tp + fn else 0.0
|
|
61
|
+
return precision, recall, false_positives, false_negatives
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_benchmark_precision_and_recall_hold():
|
|
65
|
+
precision, recall, false_positives, false_negatives = score()
|
|
66
|
+
assert precision >= MIN_PRECISION, f"precision dropped to {precision:.3f}; FP: {false_positives}"
|
|
67
|
+
assert recall >= MIN_RECALL, f"recall dropped to {recall:.3f}; FN: {false_negatives}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_benchmark_covers_both_labels():
|
|
71
|
+
cases = json.loads(FIXTURE.read_text(encoding="utf-8"))
|
|
72
|
+
positives = sum(1 for c in cases if c["conflict"])
|
|
73
|
+
negatives = len(cases) - positives
|
|
74
|
+
# A benchmark that drifts to one class measures nothing.
|
|
75
|
+
assert positives >= 10, positives
|
|
76
|
+
assert negatives >= 10, negatives
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_benchmark_includes_chinese_cases():
|
|
80
|
+
cases = json.loads(FIXTURE.read_text(encoding="utf-8"))
|
|
81
|
+
chinese = [c for c in cases if any("\u4e00" <= ch <= "\u9fff" for ch in c["a"]["name"])]
|
|
82
|
+
assert len(chinese) >= 4, len(chinese)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|