agent-tune-kit 2.1.1__tar.gz → 2.2.0__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.
Files changed (120) hide show
  1. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/.codex-plugin/plugin.json +1 -1
  2. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/.gitignore +2 -1
  3. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/PKG-INFO +29 -2
  4. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/README.en.md +28 -1
  5. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/README.md +28 -2
  6. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/__init__.py +1 -1
  7. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/cli/dataset.py +4 -1
  8. agent_tune_kit-2.2.0/agent_tune_kit/cli/results.py +99 -0
  9. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/cli/visualize.py +4 -1
  10. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/datasets/prepare.py +4 -1
  11. agent_tune_kit-2.2.0/agent_tune_kit/datasets/truth.py +179 -0
  12. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/datasets/updates.py +53 -12
  13. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/datasets/validation.py +3 -0
  14. agent_tune_kit-2.2.0/agent_tune_kit/execution/calibration.py +135 -0
  15. agent_tune_kit-2.2.0/agent_tune_kit/execution/rules.py +78 -0
  16. agent_tune_kit-2.2.0/agent_tune_kit/execution/runner.py +696 -0
  17. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/execution/runtime.py +27 -6
  18. agent_tune_kit-2.2.0/agent_tune_kit/execution/tracing.py +216 -0
  19. agent_tune_kit-2.2.0/agent_tune_kit/results/assessment.py +162 -0
  20. agent_tune_kit-2.2.0/agent_tune_kit/results/evidence.py +815 -0
  21. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/results/judgments.py +23 -7
  22. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/results/summary.py +43 -7
  23. agent_tune_kit-2.2.0/agent_tune_kit/results/versions.py +140 -0
  24. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/visualize/assets/dataset/app.js +77 -28
  25. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/visualize/assets/dataset/page.html +9 -0
  26. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/visualize/assets/dataset/styles.css +4 -0
  27. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/visualize/dataset.py +24 -2
  28. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/visualize/failures.py +43 -5
  29. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/visualize/history.py +7 -1
  30. agent_tune_kit-2.2.0/docs/execution-evidence-and-tracing-plan.md +902 -0
  31. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/docs/ground-truth-and-grading-assessment-2026-10-25.md +107 -3
  32. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/docs/shared-versioning-and-confirmation.md +61 -67
  33. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/docs/skill-template-pack-usage.md +22 -1
  34. agent_tune_kit-2.2.0/docs/truth-and-assessment.md +106 -0
  35. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/pyproject.toml +6 -2
  36. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/scripts/check-release.py +41 -0
  37. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/scripts/validate_skill_pack.py +10 -1
  38. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/skills/atk-dataset/SKILL.md +13 -2
  39. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/skills/atk-diagnose/SKILL.md +26 -5
  40. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/skills/atk-eval/SKILL.md +15 -6
  41. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/skills/atk-init/SKILL.md +10 -6
  42. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/skills/atk-optimize/SKILL.md +13 -3
  43. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/skills/atk-report/SKILL.md +19 -2
  44. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/skills/atk-visualize-dataset/SKILL.md +6 -0
  45. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/skills/atk-visualize-failures/SKILL.md +4 -2
  46. agent_tune_kit-2.2.0/templates/.atk/runner/eval_runner.py.md +89 -0
  47. agent_tune_kit-2.2.0/templates/.atk/runner/import_evidence.py.md +42 -0
  48. agent_tune_kit-2.2.0/tests/test_assessment_contract.py +183 -0
  49. agent_tune_kit-2.2.0/tests/test_evidence_import.py +160 -0
  50. agent_tune_kit-2.2.0/tests/test_evidence_judgments.py +63 -0
  51. agent_tune_kit-2.2.0/tests/test_evidence_packaging.py +11 -0
  52. agent_tune_kit-2.2.0/tests/test_evidence_versions.py +79 -0
  53. agent_tune_kit-2.2.0/tests/test_execution_evidence.py +196 -0
  54. agent_tune_kit-2.2.0/tests/test_execution_tracing.py +403 -0
  55. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/tests/test_generate_dataset_browser.py +1 -1
  56. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/tests/test_install_plugin.py +1 -1
  57. agent_tune_kit-2.2.0/tests/test_judge_calibration.py +81 -0
  58. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/tests/test_release_scripts.py +1 -1
  59. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/tests/test_runner_template_row_logging.py +1 -1
  60. agent_tune_kit-2.2.0/tests/test_truth_browser.py +63 -0
  61. agent_tune_kit-2.2.0/tests/test_truth_cli.py +65 -0
  62. agent_tune_kit-2.2.0/tests/test_truth_contract.py +151 -0
  63. agent_tune_kit-2.2.0/tests/test_truth_runner.py +21 -0
  64. agent_tune_kit-2.1.1/agent_tune_kit/cli/results.py +0 -32
  65. agent_tune_kit-2.1.1/agent_tune_kit/execution/rules.py +0 -40
  66. agent_tune_kit-2.1.1/agent_tune_kit/execution/runner.py +0 -379
  67. agent_tune_kit-2.1.1/agent_tune_kit/results/versions.py +0 -51
  68. agent_tune_kit-2.1.1/docs/execution-evidence-and-tracing-plan.md +0 -434
  69. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/cli/__init__.py +0 -0
  70. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/cli/__main__.py +0 -0
  71. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/cli/evaluation.py +0 -0
  72. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/cli/main.py +0 -0
  73. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/cli/output.py +0 -0
  74. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/datasets/__init__.py +0 -0
  75. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/errors.py +0 -0
  76. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/execution/__init__.py +0 -0
  77. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/execution/logs.py +0 -0
  78. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/installer.py +0 -0
  79. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/results/__init__.py +0 -0
  80. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/utils/__init__.py +0 -0
  81. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/utils/csv_io.py +0 -0
  82. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/utils/files.py +0 -0
  83. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/visualize/__init__.py +0 -0
  84. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/visualize/assets/dataset/vendor/echarts.min.js +0 -0
  85. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/visualize/assets/failures/app.js +0 -0
  86. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/visualize/assets/failures/page.html +0 -0
  87. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/visualize/assets/failures/styles.css +0 -0
  88. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/visualize/fields.py +0 -0
  89. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/visualize/render.py +0 -0
  90. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/agent_tune_kit/workspace.py +0 -0
  91. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/docs/assets/arch-en.png +0 -0
  92. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/docs/assets/arch.png +0 -0
  93. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/docs/assets/architecture.en.svg +0 -0
  94. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/docs/assets/architecture.svg +0 -0
  95. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/docs/cli-skill-modularization-plan.md +0 -0
  96. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/docs/codex_agent_tuning_prd.md +0 -0
  97. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/docs/local-context.md +0 -0
  98. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/scripts/dev-refresh-install.sh +0 -0
  99. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/scripts/install_plugin.py +0 -0
  100. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/scripts/publish-pypi.sh +0 -0
  101. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/scripts/publish-release.py +0 -0
  102. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/scripts/release-version.py +0 -0
  103. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/scripts/release-version.sh +0 -0
  104. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/skills/atk-dataset/scripts/update_dataset.py +0 -0
  105. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/skills/atk-visualize-dataset/scripts/generate_dataset_browser.py +0 -0
  106. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/skills/atk-visualize-failures/scripts/generate_failure_browser.py +0 -0
  107. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/templates/.atk/runner/failure_rule.py.md +0 -0
  108. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/templates/.gitkeep +0 -0
  109. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/tests/__init__.py +0 -0
  110. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/tests/conftest.py +0 -0
  111. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/tests/fixtures/legacy_eval_runner.py.txt +0 -0
  112. /agent_tune_kit-2.1.1/templates/.atk/runner/eval_runner.py.md → /agent_tune_kit-2.2.0/tests/fixtures/managed_v1.py.md +0 -0
  113. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/tests/test_atk_init_contract.py +0 -0
  114. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/tests/test_cli_workflow.py +0 -0
  115. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/tests/test_dataset_updates.py +0 -0
  116. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/tests/test_diagnosis.py +0 -0
  117. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/tests/test_generate_failure_browser.py +0 -0
  118. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/tests/test_local_context.py +0 -0
  119. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/tests/test_plugin_cache.py +0 -0
  120. {agent_tune_kit-2.1.1 → agent_tune_kit-2.2.0}/tests/test_runtime_contracts.py +0 -0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-tune-kit",
3
- "version": "2.1.1",
3
+ "version": "2.2.0",
4
4
  "description": "Local Codex plugin for iterative Agent tuning with guided Skills, reusable runner templates, versioned results, and static validation.",
5
5
  "author": {
6
6
  "name": "hustyichi",
@@ -36,4 +36,5 @@ node_modules/
36
36
  .pnpm-store/
37
37
 
38
38
  # Agent tune kit generated outputs
39
- .atk/
39
+ .atk/
40
+ !templates/.atk/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agent-tune-kit
3
- Version: 2.1.1
3
+ Version: 2.2.0
4
4
  Summary: Local Codex plugin for iterative Agent tuning with guided Skills, reusable runner templates, versioned results, and static validation.
5
5
  Project-URL: Homepage, https://github.com/hustyichi/agent-tune-kit
6
6
  Project-URL: Repository, https://github.com/hustyichi/agent-tune-kit
@@ -14,6 +14,12 @@ Description-Content-Type: text/markdown
14
14
 
15
15
  # Agent Tune Kit
16
16
 
17
+ Ground-truth reviews, judge calibration and assessment provenance are documented in
18
+ [the trust contract](docs/truth-and-assessment.md). Existing datasets remain readable, not automatically certified.
19
+ `atk dataset inspect/update` supports content-bound confirmations; `atk results calibrate` checks reviewed outputs
20
+ without executing the Agent or allocating an evaluation version. Rejudgments preserve history and comparisons expose
21
+ changes to their criteria and judge.
22
+
17
23
  English | [简体中文](README.md)
18
24
 
19
25
  [![PyPI](https://img.shields.io/pypi/v/agent-tune-kit.svg)](https://pypi.org/project/agent-tune-kit/)
@@ -207,4 +213,25 @@ atk visualize dataset --no-open
207
213
  atk visualize failures --no-open
208
214
  ```
209
215
 
210
- These commands run from the Agent project. New project adapters delegate to the shared engine; existing full runners remain executable. Dataset fields, IDs, result paths, unknown verdicts, logs and offline review pages retain their contracts. Use `--help` on each command for its options.
216
+ These commands run from the Agent project. New project adapters delegate to the shared engine; legacy full runners require migration before writing alongside new evidence Runs. Dataset fields, IDs, result paths, unknown verdicts, logs and offline review pages retain their contracts. Use `--help` on each command for its options.
217
+
218
+
219
+ ## 本地执行证据 / Local execution evidence
220
+
221
+ V2 adapters declare `ATK_ADAPTER_VERSION = 2` and `EVIDENCE.retain(role, value)`.
222
+ Each execution saves independent input/output, context, structured events and artifacts beneath
223
+ `.atk/results/vN/executions/<id>/`; shared Cases map multiple selected samples to one directory.
224
+ Retention runs before persistence. Existing V1 adapters remain explicit; no historical migration is automatic.
225
+
226
+ ```sh
227
+ atk results inspect --json
228
+ atk results evidence --version v1 --atk-id 1 --json
229
+ atk results evidence --execution-dir ./copied-execution --json
230
+ atk results import-evidence --source ./native-run --adapter ./importer.py --json
231
+ ```
232
+
233
+ `inspect` separates latest Run from the evaluation target. Evidence-only imports do not displace the latter.
234
+ Diagnosis and reports bind `--version`; changed evidence invalidates dependent judgments. Import adapters are
235
+ trusted Python code, not a sandbox. No Agent call, remote fetch, automatic cleanup or post-seal supplementation
236
+ is performed. Declared capture coverage is distinct from successful persistence and business verdicts.
237
+ See [execution evidence plan](docs/execution-evidence-and-tracing-plan.md) for interfaces, source limitations, recovery and validation.
@@ -1,5 +1,11 @@
1
1
  # Agent Tune Kit
2
2
 
3
+ Ground-truth reviews, judge calibration and assessment provenance are documented in
4
+ [the trust contract](docs/truth-and-assessment.md). Existing datasets remain readable, not automatically certified.
5
+ `atk dataset inspect/update` supports content-bound confirmations; `atk results calibrate` checks reviewed outputs
6
+ without executing the Agent or allocating an evaluation version. Rejudgments preserve history and comparisons expose
7
+ changes to their criteria and judge.
8
+
3
9
  English | [简体中文](README.md)
4
10
 
5
11
  [![PyPI](https://img.shields.io/pypi/v/agent-tune-kit.svg)](https://pypi.org/project/agent-tune-kit/)
@@ -193,4 +199,25 @@ atk visualize dataset --no-open
193
199
  atk visualize failures --no-open
194
200
  ```
195
201
 
196
- These commands run from the Agent project. New project adapters delegate to the shared engine; existing full runners remain executable. Dataset fields, IDs, result paths, unknown verdicts, logs and offline review pages retain their contracts. Use `--help` on each command for its options.
202
+ These commands run from the Agent project. New project adapters delegate to the shared engine; legacy full runners require migration before writing alongside new evidence Runs. Dataset fields, IDs, result paths, unknown verdicts, logs and offline review pages retain their contracts. Use `--help` on each command for its options.
203
+
204
+
205
+ ## 本地执行证据 / Local execution evidence
206
+
207
+ V2 adapters declare `ATK_ADAPTER_VERSION = 2` and `EVIDENCE.retain(role, value)`.
208
+ Each execution saves independent input/output, context, structured events and artifacts beneath
209
+ `.atk/results/vN/executions/<id>/`; shared Cases map multiple selected samples to one directory.
210
+ Retention runs before persistence. Existing V1 adapters remain explicit; no historical migration is automatic.
211
+
212
+ ```sh
213
+ atk results inspect --json
214
+ atk results evidence --version v1 --atk-id 1 --json
215
+ atk results evidence --execution-dir ./copied-execution --json
216
+ atk results import-evidence --source ./native-run --adapter ./importer.py --json
217
+ ```
218
+
219
+ `inspect` separates latest Run from the evaluation target. Evidence-only imports do not displace the latter.
220
+ Diagnosis and reports bind `--version`; changed evidence invalidates dependent judgments. Import adapters are
221
+ trusted Python code, not a sandbox. No Agent call, remote fetch, automatic cleanup or post-seal supplementation
222
+ is performed. Declared capture coverage is distinct from successful persistence and business verdicts.
223
+ See [execution evidence plan](docs/execution-evidence-and-tracing-plan.md) for interfaces, source limitations, recovery and validation.
@@ -1,5 +1,10 @@
1
1
  # Agent Tune Kit
2
2
 
3
+ 真值复核、裁判校准和判定追溯见[可信度契约](docs/truth-and-assessment.md):数据集可按答案字段记录来源和
4
+ 有效确认,输入/答案/标准变化会使确认过期;`atk dataset update --review-path dataset_review.csv` 可只导入
5
+ 确认而不改答案。`atk results calibrate` 用已复核输出检查裁判,不调用 Agent 或创建评测轮次。
6
+ 判定可绑定标准与裁判快照,重判保存历史,比较显式列出可比性差异。旧数据可读,但不自动标为已确认。
7
+
3
8
  简体中文 | [English](README.en.md)
4
9
 
5
10
  [![PyPI](https://img.shields.io/pypi/v/agent-tune-kit.svg)](https://pypi.org/project/agent-tune-kit/)
@@ -176,7 +181,7 @@ ATK 可选读取本地私有的 `.atk/context.md`,用于保存用户沟通后
176
181
  ## 能力评估
177
182
 
178
183
  - [Ground truth 与评分方案支持评估(2026-10-25)](docs/ground-truth-and-grading-assessment-2026-10-25.md):基于当前源码与 44 项相关测试,梳理数据准备、人工校准和判定支持,区分小规模调优可用性与可信评分体系的缺口;未重新验收业务调优效果。
179
- - [执行现场与调用轨迹增强方案(2026-10-25)](docs/execution-evidence-and-tracing-plan.md):基于模块化核心,设计完整同进程采集、运行快照、日志与调用接入、证据读取及兼容迁移;范围已确认,尚未实现。
184
+ - [执行项现场与调用轨迹改造实现方案(2026-10-26)](docs/execution-evidence-and-tracing-plan.md):明确每项独立现场、V2 托管采集及外部 runner 本地证据导入,结合 Magic 双 Case 实践补齐来源身份、文件变更与验收;首版不做封存后补采,按 P0–P5 实施,详见方案中的验收状态。
180
185
 
181
186
  兼容一次性安装命令 `uvx --from agent-tune-kit atk install`;日常使用 Skill 需要 PATH 中持续可用的 `atk` 命令。
182
187
 
@@ -198,4 +203,25 @@ atk visualize dataset --no-open
198
203
  atk visualize failures --no-open
199
204
  ```
200
205
 
201
- 新生成的项目适配文件使用统一执行引擎,已有完整 runner 保留兼容执行路径。数据字段、ID、结果目录、未判定状态、日志和离线浏览页遵循原有约定。各命令参数见对应 `--help`。
206
+ 新生成的项目适配文件使用统一执行引擎,旧完整 runner 与新证据现场共存时需先满足写入保护,否则启动前提示迁移。数据字段、ID、结果目录、未判定状态、日志和离线浏览页遵循原有约定。各命令参数见对应 `--help`。
207
+
208
+
209
+ ## 本地执行证据 / Local execution evidence
210
+
211
+ V2 adapters declare `ATK_ADAPTER_VERSION = 2` and `EVIDENCE.retain(role, value)`.
212
+ Each execution saves independent input/output, context, structured events and artifacts beneath
213
+ `.atk/results/vN/executions/<id>/`; shared Cases map multiple selected samples to one directory.
214
+ Retention runs before persistence. Existing V1 adapters remain explicit; no historical migration is automatic.
215
+
216
+ ```sh
217
+ atk results inspect --json
218
+ atk results evidence --version v1 --atk-id 1 --json
219
+ atk results evidence --execution-dir ./copied-execution --json
220
+ atk results import-evidence --source ./native-run --adapter ./importer.py --json
221
+ ```
222
+
223
+ `inspect` separates latest Run from the evaluation target. Evidence-only imports do not displace the latter.
224
+ Diagnosis and reports bind `--version`; changed evidence invalidates dependent judgments. Import adapters are
225
+ trusted Python code, not a sandbox. No Agent call, remote fetch, automatic cleanup or post-seal supplementation
226
+ is performed. Declared capture coverage is distinct from successful persistence and business verdicts.
227
+ See [execution evidence plan](docs/execution-evidence-and-tracing-plan.md) for interfaces, source limitations, recovery and validation.
@@ -1,3 +1,3 @@
1
1
  """Agent Tune Kit package."""
2
2
 
3
- __version__ = "2.1.1"
3
+ __version__ = "2.2.0"
@@ -6,6 +6,7 @@ from agent_tune_kit.workspace import DATASET_PATH
6
6
  def add_options(parser):
7
7
  parser.add_argument("--dataset-path", default=str(DATASET_PATH))
8
8
  parser.add_argument("--field", default="ground_truth")
9
+ parser.add_argument("--input-fields", nargs="+")
9
10
  parser.add_argument("--home-dir")
10
11
  mode = parser.add_mutually_exclusive_group()
11
12
  mode.add_argument("--review-path")
@@ -27,7 +28,7 @@ def register(subparsers):
27
28
  add_options(commands.add_parser("inspect"))
28
29
  update = commands.add_parser("update")
29
30
  add_options(update)
30
- update.add_argument("--updates-path", required=True)
31
+ update.add_argument("--updates-path")
31
32
 
32
33
 
33
34
  def execute(args):
@@ -39,6 +40,7 @@ def execute(args):
39
40
  review_path=args.review_path,
40
41
  field=args.field,
41
42
  home_dir=args.home_dir,
43
+ input_fields=args.input_fields,
42
44
  )
43
45
  if args.action == "inspect":
44
46
  return datasets.inspect_dataset(**options)
@@ -61,6 +63,7 @@ def legacy_main(argv=None):
61
63
  review_path=args.review_path,
62
64
  field=args.field,
63
65
  home_dir=args.home_dir,
66
+ input_fields=args.input_fields,
64
67
  )
65
68
  try:
66
69
  if args.dump_context or not args.updates_path:
@@ -0,0 +1,99 @@
1
+ from agent_tune_kit import results
2
+ from agent_tune_kit.execution import run_rule
3
+
4
+
5
+ def register(subparsers):
6
+ commands = subparsers.add_parser("results", help="inspect and judge versioned results").add_subparsers(
7
+ dest="action", required=True
8
+ )
9
+ calibration = commands.add_parser(
10
+ "calibrate", help="check a judge against reviewed outputs without running the Agent"
11
+ )
12
+ calibration.add_argument("--samples-path", required=True)
13
+ calibration.add_argument("--output-path", required=True)
14
+ mode = calibration.add_mutually_exclusive_group(required=True)
15
+ mode.add_argument("--rule-path")
16
+ mode.add_argument("--judgments-path")
17
+ calibration.add_argument("--judge-id")
18
+ calibration.add_argument("--instructions")
19
+ calibration.add_argument("--python")
20
+ calibration.add_argument("--json", action="store_true")
21
+ for name in ["inspect", "run-rule", "apply-judgments", "compare", "evidence", "import-evidence"]:
22
+ parser = commands.add_parser(name)
23
+ parser.add_argument("--results-dir", default=".atk/results")
24
+ parser.add_argument("--json", action="store_true")
25
+ if name != "import-evidence":
26
+ parser.add_argument("--version")
27
+ if name == "run-rule":
28
+ parser.add_argument("--rule-path", default=".atk/runner/failure_rule.py")
29
+ parser.add_argument("--python")
30
+ elif name == "apply-judgments":
31
+ parser.add_argument("--judgments-path", required=True)
32
+ parser.add_argument("--source-results")
33
+ elif name == "compare":
34
+ parser.add_argument("--ids", nargs="+", required=True)
35
+ elif name == "import-evidence":
36
+ parser.add_argument("--source", required=True)
37
+ parser.add_argument("--adapter", required=True, help="explicit trusted Python adapter (not sandboxed)")
38
+ elif name == "evidence":
39
+ parser.add_argument("--execution-dir")
40
+ parser.add_argument("--atk-id")
41
+ parser.add_argument("--execution-id")
42
+ parser.add_argument("--event-id")
43
+ parser.add_argument("--artifact-id")
44
+ parser.add_argument("--after-seq", type=int)
45
+ parser.add_argument("--offset", type=int, default=0)
46
+ parser.add_argument("--limit", type=int)
47
+
48
+
49
+ def execute(args):
50
+ if args.action == "calibrate":
51
+ from agent_tune_kit.execution.calibration import launch_calibration
52
+
53
+ return launch_calibration(
54
+ **{
55
+ key: getattr(args, key)
56
+ for key in (
57
+ "samples_path",
58
+ "output_path",
59
+ "rule_path",
60
+ "judgments_path",
61
+ "judge_id",
62
+ "instructions",
63
+ "python",
64
+ )
65
+ }
66
+ )
67
+ if args.action == "import-evidence":
68
+ from agent_tune_kit.results.evidence import import_evidence
69
+
70
+ return import_evidence(args.source, args.adapter, args.results_dir)
71
+ if args.action == "evidence":
72
+ from agent_tune_kit.results.evidence import query_evidence
73
+
74
+ return query_evidence(
75
+ **{
76
+ key: getattr(args, key)
77
+ for key in [
78
+ "results_dir",
79
+ "version",
80
+ "execution_dir",
81
+ "atk_id",
82
+ "execution_id",
83
+ "event_id",
84
+ "artifact_id",
85
+ "after_seq",
86
+ "offset",
87
+ "limit",
88
+ ]
89
+ }
90
+ )
91
+ if args.action == "inspect":
92
+ return results.inspect_results(args.results_dir, version=args.version)
93
+ if args.action == "compare":
94
+ return results.compare_targets(args.ids, args.results_dir, version=args.version)
95
+ if args.action == "apply-judgments":
96
+ return results.apply_judgments(
97
+ args.judgments_path, source_results=args.source_results, results_dir=args.results_dir, version=args.version
98
+ )
99
+ return run_rule(rule_path=args.rule_path, results_dir=args.results_dir, python=args.python, version=args.version)
@@ -16,6 +16,7 @@ def add_options(parser, kind):
16
16
  parser.add_argument("--dataset-path", default=".atk/datasets/dataset.csv")
17
17
  else:
18
18
  parser.add_argument("--results-dir", default=".atk/results")
19
+ parser.add_argument("--version")
19
20
  parser.add_argument("--no-report", action="store_true")
20
21
  parser.add_argument("--no-history", action="store_true")
21
22
 
@@ -32,7 +33,9 @@ def execute(args):
32
33
  options = dict(overwrite=args.overwrite, open_browser=args.open_browser)
33
34
  if args.action == "dataset":
34
35
  return generate_dataset(args.dataset_path, **options)
35
- return generate_failures(args.results_dir, no_report=args.no_report, no_history=args.no_history, **options)
36
+ return generate_failures(
37
+ args.results_dir, no_report=args.no_report, no_history=args.no_history, version=args.version, **options
38
+ )
36
39
 
37
40
 
38
41
  def legacy_main(kind, argv=None):
@@ -4,6 +4,7 @@ from agent_tune_kit.errors import UserActionRequired
4
4
  from agent_tune_kit.utils.csv_io import parse_csv, write_csv_atomic
5
5
  from agent_tune_kit.workspace import DATASET_PATH
6
6
 
7
+ from .truth import TRUTH_FIELD
7
8
  from .validation import validate_dataset_rows
8
9
 
9
10
 
@@ -15,8 +16,10 @@ def prepare_dataset(source, dataset_path=DATASET_PATH, *, append=False, overwrit
15
16
  raise UserActionRequired("Append requires an existing canonical dataset.")
16
17
  if append:
17
18
  validate_dataset_rows(old_fields, old_rows)
18
- if set(fields) - {"atk_id"} != set(old_fields) - {"atk_id"}:
19
+ if set(fields) - {"atk_id", TRUTH_FIELD} != set(old_fields) - {"atk_id", TRUTH_FIELD}:
19
20
  raise UserActionRequired("Append fields differ; confirm the field mapping before appending.")
21
+ if TRUTH_FIELD in fields and TRUTH_FIELD not in old_fields:
22
+ old_fields = old_fields + [TRUTH_FIELD]
20
23
  if "atk_id" not in fields:
21
24
  fields = fields + ["atk_id"]
22
25
  start = max((int(row["atk_id"]) for row in old_rows), default=0) + 1 if append else 1
@@ -0,0 +1,179 @@
1
+ """Content-bound review declarations; integrity never implies domain correctness."""
2
+
3
+ import hashlib
4
+ import json
5
+ from datetime import UTC, datetime
6
+ from pathlib import Path
7
+
8
+ from agent_tune_kit.errors import UserActionRequired
9
+
10
+ TRUTH_FIELD = "atk_truth"
11
+ REVIEW_STATES = {"pending", "confirmed", "disputed"}
12
+ STANDARD_SECTIONS = {"Agent Behavior Standard", "Ground Truth Standard"}
13
+ INPUT_CANDIDATES = ("input", "query", "question", "prompt", "task", "user_input", "instruction")
14
+
15
+
16
+ def digest(value):
17
+ return hashlib.sha256(
18
+ json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode()
19
+ ).hexdigest()
20
+
21
+
22
+ def standard_snapshot(path):
23
+ path = Path(path)
24
+ text = path.read_text(encoding="utf-8") if path.is_file() else ""
25
+ if path.name == "context.md":
26
+ lines, active = [], False
27
+ for line in text.splitlines():
28
+ if line.startswith("## "):
29
+ active = line[3:].strip() in STANDARD_SECTIONS
30
+ if active:
31
+ lines.append(line.rstrip())
32
+ text = "\n".join(lines).strip()
33
+ return {"text": text, "sha256": digest(text)}
34
+
35
+
36
+ def validate_inputs(row, field, inputs):
37
+ if field not in row or field in {TRUTH_FIELD, "atk_id"}:
38
+ raise UserActionRequired(f"Unknown or reserved answer field: {field}")
39
+ if (
40
+ not isinstance(inputs, list)
41
+ or not inputs
42
+ or any(not isinstance(k, str) for k in inputs)
43
+ or len(set(inputs)) != len(inputs)
44
+ or any(k not in row or k in {field, TRUTH_FIELD, "atk_id"} for k in inputs)
45
+ ):
46
+ raise UserActionRequired(
47
+ "Review requires explicit, unique business input fields, excluding answer and ATK metadata."
48
+ )
49
+
50
+
51
+ def subject_digest(row, field, inputs, standard, *, source="", evidence=""):
52
+ validate_inputs(row, field, inputs)
53
+ return digest(
54
+ {
55
+ "atk_id": row.get("atk_id"),
56
+ "field": field,
57
+ "answer": row[field],
58
+ "inputs": {k: row[k] for k in inputs},
59
+ "standard": standard["sha256"],
60
+ "source": source,
61
+ "evidence": evidence,
62
+ }
63
+ )
64
+
65
+
66
+ def truth_records(row):
67
+ raw = row.get(TRUTH_FIELD, "")
68
+ if not raw:
69
+ return {}
70
+ try:
71
+ data = json.loads(raw)
72
+ if data.get("schema") != 1 or not isinstance(data.get("fields"), dict):
73
+ raise ValueError("Unknown truth schema")
74
+ for field, record in data["fields"].items():
75
+ validate_inputs(row, field, record.get("input_fields"))
76
+ standard = record["standard"]
77
+ if (
78
+ record["status"] not in REVIEW_STATES
79
+ or not isinstance(standard["text"], str)
80
+ or standard["sha256"] != digest(standard["text"])
81
+ or not isinstance(record["subject_sha256"], str)
82
+ or len(record["subject_sha256"]) != 64
83
+ ):
84
+ raise ValueError("Invalid review binding")
85
+ if record["status"] == "confirmed" and any(
86
+ not isinstance(record.get(k), str) or not record[k].strip()
87
+ for k in ("source", "evidence", "reviewer", "reviewed_at")
88
+ ):
89
+ raise ValueError("Confirmed reviews require source, evidence, reviewer and time")
90
+ return data["fields"]
91
+ except (ValueError, TypeError, AttributeError, KeyError) as exc:
92
+ raise UserActionRequired(f"Invalid atk_truth for {row.get('atk_id')}: {exc}") from exc
93
+
94
+
95
+ def truth_status(row, field, *, standard=None):
96
+ record = truth_records(row).get(field)
97
+ if record is None:
98
+ return {"field": field, "status": "unreviewed", "reason": "No review recorded"}
99
+ current = standard if standard is not None else record["standard"]
100
+ stale = (
101
+ subject_digest(
102
+ row,
103
+ field,
104
+ record["input_fields"],
105
+ current,
106
+ source=record.get("source", ""),
107
+ evidence=record.get("evidence", ""),
108
+ )
109
+ != record["subject_sha256"]
110
+ )
111
+ return {
112
+ "field": field,
113
+ "status": "stale" if stale else record["status"],
114
+ "reason": "Input, answer or relevant standard changed" if stale else "Recorded review declaration",
115
+ "record": record,
116
+ }
117
+
118
+
119
+ def review_context(path, rows, field, input_fields=None):
120
+ path = Path(path)
121
+ standard = standard_snapshot(path.parent.parent / "context.md")
122
+ inputs = input_fields or next(([k] for k in INPUT_CANDIDATES if rows and k in rows[0]), [])
123
+ truth = []
124
+ for row in rows:
125
+ state = truth_status(row, field, standard=standard)
126
+ state["atk_id"] = row.get("atk_id", "")
127
+ state["input_fields"] = input_fields or state.get("record", {}).get("input_fields", inputs)
128
+ truth.append(state)
129
+ return {
130
+ "dataset_sha256": hashlib.sha256(path.read_bytes()).hexdigest(),
131
+ "dataset_scope": digest(str(path.resolve())),
132
+ "standard": standard,
133
+ "truth": truth,
134
+ "truth_counts": {
135
+ s: sum(r["status"] == s for r in truth) for s in ("unreviewed", "pending", "confirmed", "disputed", "stale")
136
+ },
137
+ }
138
+
139
+
140
+ def validate_review_binding(review, context):
141
+ for key in ("dataset_sha256", "dataset_scope"):
142
+ if review.get(key) != context[key]:
143
+ raise UserActionRequired("Review is stale or belongs to another dataset; inspect the current data again.")
144
+ if review.get("standard_sha256") != context["standard"]["sha256"]:
145
+ raise UserActionRequired("Review standard changed; inspect the current standard again.")
146
+
147
+
148
+ def apply_review(row, review, standard, *, answer_changed=False):
149
+ field = review.get("field", "")
150
+ try:
151
+ inputs = json.loads(review.get("input_fields", "[]"))
152
+ except (ValueError, TypeError) as exc:
153
+ raise UserActionRequired("input_fields must be a JSON array.") from exc
154
+ validate_inputs(row, field, inputs)
155
+ status = review.get("review_status")
156
+ if status not in REVIEW_STATES:
157
+ raise UserActionRequired("review_status must be pending, confirmed or disputed.")
158
+ if status == "confirmed" and (
159
+ not row[field].strip() or any(not review.get(k, "").strip() for k in ("source", "evidence", "reviewer"))
160
+ ):
161
+ raise UserActionRequired("Confirmation requires a non-empty answer, source, evidence and reviewer.")
162
+ records = truth_records(row)
163
+ record = {
164
+ "status": "pending" if answer_changed else status,
165
+ "input_fields": inputs,
166
+ "source": review.get("source", ""),
167
+ "evidence": review.get("evidence", ""),
168
+ "reviewer": review.get("reviewer", ""),
169
+ "standard": standard,
170
+ "subject_sha256": subject_digest(
171
+ row, field, inputs, standard, source=review.get("source", ""), evidence=review.get("evidence", "")
172
+ ),
173
+ }
174
+ old = records.get(field, {})
175
+ # Reapplying the same declaration does not manufacture a new review event.
176
+ record["reviewed_at"] = old.get("reviewed_at") if all(old.get(k) == v for k, v in record.items()) else None
177
+ record["reviewed_at"] = record["reviewed_at"] or datetime.now(UTC).isoformat()
178
+ records[field] = record
179
+ row[TRUTH_FIELD] = json.dumps({"schema": 1, "fields": records}, ensure_ascii=False, sort_keys=True)
@@ -6,6 +6,7 @@ from agent_tune_kit.utils.csv_io import parse_csv
6
6
  from agent_tune_kit.utils.csv_io import write_csv_atomic as write_dataset_atomic
7
7
  from agent_tune_kit.workspace import DATASET_PATH
8
8
 
9
+ from .truth import TRUTH_FIELD, apply_review, review_context, validate_review_binding
9
10
  from .validation import find_duplicates, parse_dataset_csv
10
11
 
11
12
  REVIEW_FILENAME = "dataset_review.csv"
@@ -18,7 +19,7 @@ def parse_review_csv(path: Path, dataset_rows: list[dict[str, str]]) -> tuple[li
18
19
  missing = [field for field in REVIEW_REQUIRED_FIELDS if field not in fieldnames]
19
20
  if missing:
20
21
  raise UserActionRequired(f"{path.as_posix()} is missing required review fields: {', '.join(missing)}")
21
- review_rows = [row for row in rows if (row.get("review_feedback") or "").strip()]
22
+ review_rows = [row for row in rows if (row.get("review_feedback") or "").strip() or row.get("review_status")]
22
23
  if not review_rows:
23
24
  raise UserActionRequired(f"{path.as_posix()} contains no non-empty review_feedback values.")
24
25
  dataset_ids = {row["atk_id"] for row in dataset_rows}
@@ -27,9 +28,9 @@ def parse_review_csv(path: Path, dataset_rows: list[dict[str, str]]) -> tuple[li
27
28
  raise UserActionRequired(
28
29
  f"{path.as_posix()} contains review atk_id values absent from dataset.csv: {', '.join(unknown)}"
29
30
  )
30
- duplicates = find_duplicates((row.get("atk_id") or "").strip() for row in review_rows)
31
+ duplicates = find_duplicates((row.get("atk_id", ""), row.get("field", "")) for row in review_rows)
31
32
  if duplicates:
32
- raise UserActionRequired(f"{path.as_posix()} contains duplicate review atk_id values: {', '.join(duplicates)}")
33
+ raise UserActionRequired(f"{path.as_posix()} contains duplicate review targets: {duplicates}")
33
34
  return fieldnames, review_rows
34
35
 
35
36
 
@@ -82,7 +83,11 @@ def parse_updates_csv(
82
83
  if not updates:
83
84
  raise UserActionRequired(f"{path.as_posix()} contains no non-empty {field} updates.")
84
85
  dataset_ids = {row["atk_id"] for row in dataset_rows}
85
- review_ids = {row["atk_id"] for row in review_rows} if review_rows is not None else dataset_ids
86
+ review_ids = (
87
+ {row["atk_id"] for row in review_rows if not row.get("field") or row["field"] == field}
88
+ if review_rows is not None
89
+ else dataset_ids
90
+ )
86
91
  unknown = sorted({row["atk_id"] for row in updates if row["atk_id"] not in dataset_ids})
87
92
  if unknown:
88
93
  raise UserActionRequired(f"{path.as_posix()} contains unknown atk_id values: {', '.join(unknown)}")
@@ -144,15 +149,18 @@ def build_context(
144
149
  }
145
150
 
146
151
 
147
- def inspect_dataset(dataset_path=DATASET_PATH, *, direct=False, review_path=None, home_dir=None, field="ground_truth"):
152
+ def inspect_dataset(
153
+ dataset_path=DATASET_PATH, *, direct=False, review_path=None, home_dir=None, field="ground_truth", input_fields=None
154
+ ):
148
155
  dataset_path = Path(dataset_path)
149
- if not field.strip() or field == "atk_id":
156
+ if not field.strip() or field in {"atk_id", TRUTH_FIELD}:
150
157
  raise UserActionRequired("Answer field must be non-empty; cannot update atk_id.")
151
158
  if not dataset_path.is_file():
152
159
  raise UserActionRequired(f"Dataset {dataset_path} is missing; run atk-dataset or atk-init first.")
153
160
  _, rows = parse_dataset_csv(dataset_path)
161
+ trust = review_context(dataset_path, rows, field, input_fields)
154
162
  if direct:
155
- return {"dataset_path": str(dataset_path), "field": field, "rows": rows}
163
+ return {"dataset_path": str(dataset_path), "field": field, "rows": rows, **trust}
156
164
  review = resolve_review_path(
157
165
  str(review_path) if review_path else None,
158
166
  dataset_path.parent,
@@ -160,24 +168,57 @@ def inspect_dataset(dataset_path=DATASET_PATH, *, direct=False, review_path=None
160
168
  rows,
161
169
  )
162
170
  _, feedback = parse_review_csv(review, rows)
163
- return build_context(dataset_path, review, rows, feedback)
171
+ for row in feedback:
172
+ if row.get("review_status") or row.get("dataset_sha256"):
173
+ validate_review_binding(row, trust)
174
+ return {**build_context(dataset_path, review, rows, feedback), **trust}
164
175
 
165
176
 
166
177
  def update_dataset(
167
- updates_path, dataset_path=DATASET_PATH, *, direct=False, review_path=None, home_dir=None, field="ground_truth"
178
+ updates_path=None,
179
+ dataset_path=DATASET_PATH,
180
+ *,
181
+ direct=False,
182
+ review_path=None,
183
+ home_dir=None,
184
+ field="ground_truth",
185
+ input_fields=None,
168
186
  ):
169
- context = inspect_dataset(dataset_path, direct=direct, review_path=review_path, home_dir=home_dir, field=field)
187
+ context = inspect_dataset(
188
+ dataset_path, direct=direct, review_path=review_path, home_dir=home_dir, field=field, input_fields=input_fields
189
+ )
170
190
  fields, rows = parse_dataset_csv(Path(dataset_path))
191
+ before = [dict(row) for row in rows]
171
192
  feedback = None
172
193
  if not direct:
173
194
  _, feedback = parse_review_csv(Path(context["review_path"]), rows)
174
- updates = parse_updates_csv(Path(updates_path), rows, feedback, field)
195
+ updates = parse_updates_csv(Path(updates_path), rows, feedback, field) if updates_path else []
196
+ if not updates and not any(r.get("review_status") for r in feedback or []):
197
+ raise UserActionRequired("Provide answer updates or explicit content-bound review decisions.")
175
198
  output_fields, output_rows, count = apply_updates(fields, rows, updates, field)
176
- if count:
199
+ original = {row["atk_id"]: row for row in before}
200
+ by_id = {row["atk_id"]: row for row in output_rows}
201
+ for review in feedback or []:
202
+ if review.get("review_status"):
203
+ row = by_id[review["atk_id"]]
204
+ apply_review(
205
+ row,
206
+ review,
207
+ context["standard"],
208
+ answer_changed=original[row["atk_id"]].get(review.get("field")) != row.get(review.get("field")),
209
+ )
210
+ if TRUTH_FIELD not in output_fields:
211
+ output_fields.append(TRUTH_FIELD)
212
+ reviewed = sum(a.get(TRUTH_FIELD, "") != b.get(TRUTH_FIELD, "") for a, b in zip(before, output_rows, strict=True))
213
+ current = review_context(Path(dataset_path), before, field)
214
+ if any(current[k] != context[k] for k in ("dataset_sha256", "standard")):
215
+ raise UserActionRequired("Dataset or standard changed during review; nothing was written.")
216
+ if count or reviewed:
177
217
  write_dataset_atomic(Path(dataset_path), output_fields, output_rows)
178
218
  result = {
179
219
  "dataset": str(dataset_path),
180
220
  "updated_rows": count,
221
+ "reviewed_rows": reviewed,
181
222
  f"{field}_column": "existing" if field in fields else "appended",
182
223
  }
183
224
  if not direct:
@@ -4,6 +4,8 @@ from typing import Any
4
4
  from agent_tune_kit.errors import UserActionRequired
5
5
  from agent_tune_kit.utils.csv_io import parse_csv
6
6
 
7
+ from .truth import truth_records
8
+
7
9
 
8
10
  def validate_dataset_rows(fieldnames: list[str], rows: list[dict[str, str]]) -> None:
9
11
  if "atk_id" not in fieldnames:
@@ -11,6 +13,7 @@ def validate_dataset_rows(fieldnames: list[str], rows: list[dict[str, str]]) ->
11
13
  seen: set[str] = set()
12
14
  invalid: list[str] = []
13
15
  for row in rows:
16
+ truth_records(row)
14
17
  atk_id = (row.get("atk_id") or "").strip()
15
18
  if not atk_id or not atk_id.isdigit() or int(atk_id) <= 0 or atk_id in seen:
16
19
  invalid.append(atk_id or "<empty>")