oh-my-knowledge 0.51.0 → 0.51.2
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.
- package/dist/assets/agent-skills/omk/SKILL.md +2 -0
- package/dist/assets/agent-skills/omk/references/commands.md +2 -2
- package/dist/authoring/generator.d.ts +15 -5
- package/dist/authoring/generator.js +181 -18
- package/dist/authoring/sample-fixer.d.ts +2 -0
- package/dist/authoring/sample-fixer.js +19 -5
- package/dist/cli/commands/sample.js +11 -4
- package/dist/eval-core/dependency-checker.js +30 -18
- package/dist/eval-core/evaluation-execution.js +11 -2
- package/dist/eval-core/fact-checker.d.ts +15 -2
- package/dist/eval-core/fact-checker.js +75 -20
- package/dist/eval-core/mock-hook.cjs +40 -1
- package/dist/eval-core/mocks-runtime.js +39 -2
- package/dist/eval-core/task-planner.d.ts +2 -2
- package/dist/eval-core/task-planner.js +7 -7
- package/dist/eval-workflows/evaluation-pipeline.js +2 -0
- package/dist/eval-workflows/run-evaluation.js +2 -1
- package/dist/executors/capabilities.d.ts +15 -0
- package/dist/executors/capabilities.js +64 -0
- package/dist/executors/index.d.ts +1 -0
- package/dist/executors/index.js +4 -1
- package/dist/observability/codex-exec-command.d.ts +7 -0
- package/dist/observability/codex-exec-command.js +134 -0
- package/dist/observability/codex-trace-adapter.js +16 -6
- package/dist/observability/trace-attribution.js +11 -107
- package/dist/server/skill-insights.js +1 -1
- package/dist/shared/sample-contract.d.ts +1 -0
- package/dist/shared/sample-contract.js +35 -0
- package/dist/shared/tool-identity.d.ts +8 -0
- package/dist/shared/tool-identity.js +13 -0
- package/dist/types/eval.d.ts +7 -6
- package/dist/types/executor.d.ts +3 -2
- package/package.json +4 -4
|
@@ -123,6 +123,8 @@ omk sample skills/my-skill/SKILL.md --focus "重点覆盖搜索失败 / 权限
|
|
|
123
123
|
omk sample --batch
|
|
124
124
|
```
|
|
125
125
|
|
|
126
|
+
目标执行器不支持工具拦截时,`omk sample` 会自动生成无 mock 用例。当前 `codex` / `codex-sdk` 属于这种情况;不要手工补 `mocks` 或 `mock_hit`。已有 mocks 用例会被 `omk eval` 在模型调用前拒绝,避免把执行器能力缺口误判成模型失败。`environment.files_available` 只提供题设上下文,不会在 `cwd` 物化文件。
|
|
127
|
+
|
|
126
128
|
输出位置:目录 skill(`<skill>/SKILL.md`)→ `<skill>/.omk/samples.json`(标准);扁平 `.md` 单次生成 → 当前目录 `eval-samples.json`(项目级兜底);扁平 `.md` 的 `--batch` 兼容生成 `<skill-dir>/<name>.eval-samples.json`。
|
|
127
129
|
|
|
128
130
|
### 观测真实使用
|
|
@@ -602,7 +602,7 @@ omk sample [skillPath] [flags]
|
|
|
602
602
|
- `--from-traces` `boolean`:from-traces 模式:从 observe inbox 的失败信号回流生成评测用例草稿(provenance: production-trace),落草稿待人工 review。
|
|
603
603
|
- `--lang` `option` (默认 `zh`):输出语言 zh|en,优先级 CLI > OMK_LANG env > zh。
|
|
604
604
|
- `--model` `option`:生成 LLM model 名。Codex 自动读取本机配置;也可用 OMK_MODEL 设置环境偏好。
|
|
605
|
-
- `--no-mock` `boolean`:不生成 mocks
|
|
605
|
+
- `--no-mock` `boolean`:不生成 mocks。执行器不支持工具拦截时会自动启用,避免产生必然失败的 mock_hit。
|
|
606
606
|
- `--observations-dir` `option`:observe inbox 目录(from-traces 模式用),默认项目 .omk/observe-inbox。
|
|
607
607
|
- `--reports-dir` `option`:报告目录(fix 模式用),默认 ~/.oh-my-knowledge/reports。
|
|
608
608
|
- `--skill` `option`:仅从指定 skill 的 observe inbox 信号生成草稿(仅 from-traces 模式用)。
|
|
@@ -689,7 +689,7 @@ omk studio --port 8080 --no-open
|
|
|
689
689
|
| `construct` | 否 | 测的是什么构念 |
|
|
690
690
|
| `provenance` | 否 | 用例来源(`omk sample` 自动打) |
|
|
691
691
|
| `mocks` | 否 | 工具调用 mock 返回(sandbox 评测) |
|
|
692
|
-
| `environment` | 否 |
|
|
692
|
+
| `environment` | 否 | 题设环境声明(仅注入 prompt,不物化) |
|
|
693
693
|
| `tripwire` | 否 | 标记为「故意诱错」样本,failed 时 diagnostic 不建议改 skill |
|
|
694
694
|
|
|
695
695
|
完整 schema 见 [docs/specs/sample-design-spec.md](https://github.com/lizhiyao/oh-my-knowledge/blob/main/docs/specs/sample-design-spec.md)。
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ExecutorFn, Sample } from '../types/index.js';
|
|
2
2
|
import type { ObservationInboxItem } from '../types/observability.js';
|
|
3
|
-
|
|
3
|
+
export declare function sampleGenerationUsesMocks(executorName: string | undefined, noMock?: boolean): boolean;
|
|
4
|
+
export interface GenerateSamplesOptions {
|
|
4
5
|
skillContent: string;
|
|
5
6
|
count?: number;
|
|
6
7
|
model: string;
|
|
@@ -12,6 +13,8 @@ interface GenerateSamplesOptions {
|
|
|
12
13
|
focus?: string;
|
|
13
14
|
/** 不生成 mocks/mocksStrict,eval 时真实执行所有工具调用。 */
|
|
14
15
|
noMock?: boolean;
|
|
16
|
+
/** Injectable executor for tests. Defaults to createExecutor(executorName). */
|
|
17
|
+
executor?: ExecutorFn;
|
|
15
18
|
}
|
|
16
19
|
/**
|
|
17
20
|
* 拼出送给 LLM 的 user prompt。抽出来便于单测验证 focus 是否真的注入了。
|
|
@@ -26,7 +29,7 @@ export declare function buildSamplesPrompt({ skillContent, count, focus, noMock
|
|
|
26
29
|
focus?: string;
|
|
27
30
|
noMock?: boolean;
|
|
28
31
|
}): string;
|
|
29
|
-
export declare function generateSamples({ skillContent, count, model, executorName, focus, noMock }: GenerateSamplesOptions): Promise<{
|
|
32
|
+
export declare function generateSamples({ skillContent, count, model, executorName, focus, noMock, executor: injectedExecutor, }: GenerateSamplesOptions): Promise<{
|
|
30
33
|
samples: Sample[];
|
|
31
34
|
costUSD: number;
|
|
32
35
|
}>;
|
|
@@ -62,12 +65,16 @@ export declare function stratifyTraceSignals(items: TraceSignalItem[]): Stratifi
|
|
|
62
65
|
* production failures. The trace text feeds the *generator* only — never the
|
|
63
66
|
* judge prompt — so judge-prompt isolation is unaffected.
|
|
64
67
|
*/
|
|
65
|
-
export declare function buildSamplesFromTracesPrompt(items: TraceSignalItem[], count?: number
|
|
68
|
+
export declare function buildSamplesFromTracesPrompt(items: TraceSignalItem[], count?: number, options?: {
|
|
69
|
+
noMock?: boolean;
|
|
70
|
+
}): string;
|
|
66
71
|
export interface GenerateSamplesFromTracesOptions {
|
|
67
72
|
items: TraceSignalItem[];
|
|
68
73
|
count?: number;
|
|
69
74
|
model: string;
|
|
70
75
|
executorName?: string;
|
|
76
|
+
/** 不生成 mocks。目标 executor 不支持时会自动启用。 */
|
|
77
|
+
noMock?: boolean;
|
|
71
78
|
/** Injectable executor (tests). Defaults to createExecutor(executorName). */
|
|
72
79
|
executor?: ExecutorFn;
|
|
73
80
|
}
|
|
@@ -77,12 +84,15 @@ export interface GenerateSamplesFromTracesOptions {
|
|
|
77
84
|
* stamps `provenance: 'production-trace'`. Output is meant to land in a review draft,
|
|
78
85
|
* not the live dataset (the CLI enforces that).
|
|
79
86
|
*/
|
|
80
|
-
export declare function generateSamplesFromTraces({ items, count, model, executorName, executor: injectedExecutor, }: GenerateSamplesFromTracesOptions): Promise<{
|
|
87
|
+
export declare function generateSamplesFromTraces({ items, count, model, executorName, noMock, executor: injectedExecutor, }: GenerateSamplesFromTracesOptions): Promise<{
|
|
81
88
|
samples: Sample[];
|
|
82
89
|
costUSD: number;
|
|
83
90
|
}>;
|
|
84
91
|
export declare function sanitizeGeneratedSamples(samples: Sample[], opts?: {
|
|
85
92
|
skillContent?: string;
|
|
93
|
+
mockless?: boolean;
|
|
94
|
+
migrateMocklessEnvironment?: boolean;
|
|
95
|
+
preserveMocklessEnvironment?: boolean;
|
|
86
96
|
}): {
|
|
87
97
|
stripped: string[];
|
|
88
98
|
};
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createExecutor } from '../executors/index.js';
|
|
2
|
+
import { executorSupportsSampleMocks } from '../executors/capabilities.js';
|
|
2
3
|
import { DEFAULT_GATE_THRESHOLD } from '../eval-core/verdict.js';
|
|
4
|
+
import { sampleMockReferenceKeys } from '../shared/sample-contract.js';
|
|
3
5
|
const SYSTEM_PROMPT = `你是一个评测用例生成器。你的任务是根据用户提供的 skill(系统提示词)内容,生成高质量的评测用例。
|
|
4
6
|
|
|
5
7
|
样本结构决策(必须先做):先扫一遍 skill 内容判断它属于哪一类,按对应配比和数量生成。
|
|
@@ -195,14 +197,14 @@ const SYSTEM_PROMPT = `你是一个评测用例生成器。你的任务是根据
|
|
|
195
197
|
tool_input_not_contains 或 tools_not_called。
|
|
196
198
|
- { "type": "regex", "pattern": "...", "weight": 1 }
|
|
197
199
|
↑ 同 contains 限制:只用在固定格式字面量(如 SHA / UUID / 路径模板)。
|
|
198
|
-
- environment:
|
|
200
|
+
- environment: 可选,对象。**仅作 prompt 上下文的题设环境声明**,不会修改 PATH、创建文件或物化 fixture。
|
|
199
201
|
字段:
|
|
200
202
|
- cli_available: string[],已在 PATH 上的 CLI(如 ["node", "git", "code-host"])
|
|
201
203
|
- files_available: string[],已存在的文件/脚本(如 ["~/.req-tool-api.json", "$SKILL_DIR/scripts/x.js"])
|
|
202
204
|
- notes: string,自由文本兜底(如"DevAPI 凭证有效,工号 testuser001")
|
|
203
205
|
原则:
|
|
204
|
-
|
|
205
|
-
|
|
206
|
+
只有用例明确把某项环境能力作为题设前提时才写。需要读取真实内容的文件不能放在
|
|
207
|
+
files_available 里冒充 fixture:应放进 sample.cwd 下的真实 fixture,或由 mock 返回内容。
|
|
206
208
|
- mocksStrict: **必填且必须设为 true**(只要 sample 配了 mocks)。
|
|
207
209
|
原因:mocksStrict=false 时,LLM 调到没匹配 mock 的命令会**透传到真 shell**,
|
|
208
210
|
既可能真调外部接口产生副作用,也可能因二进制不存在(如 mcporter)报噪声错误污染评测信号。
|
|
@@ -223,8 +225,8 @@ const SYSTEM_PROMPT = `你是一个评测用例生成器。你的任务是根据
|
|
|
223
225
|
- 对(✅):写一条宽 mock:\`{tool:"Bash", match:{command_glob:"ls *"},
|
|
224
226
|
return:{stdout:"<模拟目录列表>", exit:0}}\` — \`command_glob\` 用 \`*\` 兜底各种
|
|
225
227
|
ls 参数变体(\`ls\` / \`ls -la\` / \`ls -d\` / \`ls /xx\` 全命中)
|
|
226
|
-
(c)
|
|
227
|
-
|
|
228
|
+
(c) 题设明确声明可用的凭证 / CLI 可写进 \`environment\`,让 LLM 不做可用性探测。
|
|
229
|
+
该字段只进 prompt,不会真的安装 CLI、创建凭证文件或改变 runtime。
|
|
228
230
|
(d) **intent-level mock(文件搜索/读取类操作)** — LLM 搜代码时会自由选择 Bash grep、
|
|
229
231
|
Grep 工具、Glob+Read 组合、甚至 Agent 子代理,逐个枚举工具写 mock 不可持续。
|
|
230
232
|
正确做法:用 \`tool: "*"\` + \`input_contains: "关键词"\` 按意图匹配:
|
|
@@ -367,6 +369,37 @@ const SYSTEM_PROMPT = `你是一个评测用例生成器。你的任务是根据
|
|
|
367
369
|
- 字符串字段(prompt / rubric / capability 等)内部如需引号,**必须用全角「」**而不是半角 \`""\`,避免漏转义破坏 JSON 解析
|
|
368
370
|
- 例:错 → \`"prompt": "查询"Daily"标签..."\`(内部 \`"\` 未转义,JSON 解析失败)
|
|
369
371
|
对 → \`"prompt": "查询「Daily」标签..."\`(全角引号,无转义压力)`;
|
|
372
|
+
const MOCKLESS_SYSTEM_OVERRIDE = `
|
|
373
|
+
|
|
374
|
+
## 目标执行器能力约束(最高优先级)
|
|
375
|
+
|
|
376
|
+
目标执行器不支持可靠的工具调用拦截。本节覆盖上文所有要求 mocks、mock_hit、
|
|
377
|
+
files_available 或正向工具调用断言的规则:
|
|
378
|
+
|
|
379
|
+
- 不要输出 mocks、mocksStrict、environment 字段。
|
|
380
|
+
- 不要输出 mock_hit、tools_called、tools_count_min、tool_input_contains、tool_output_contains。
|
|
381
|
+
- 可以保留 tools_not_called、tools_count_max、tool_input_not_contains 这类负向安全约束。
|
|
382
|
+
- 把必要的输入事实直接写进 context,把可判分结果写进 rubric 或输出内容断言。
|
|
383
|
+
- 不要假装某个文件、CLI 或工具调用已经由 fixture 物化。`;
|
|
384
|
+
function generationSystemPrompt(mockless) {
|
|
385
|
+
return mockless ? `${SYSTEM_PROMPT}${MOCKLESS_SYSTEM_OVERRIDE}` : SYSTEM_PROMPT;
|
|
386
|
+
}
|
|
387
|
+
const warnedAutoMocklessExecutors = new Set();
|
|
388
|
+
function warnAutoMockless(executorName, noMock) {
|
|
389
|
+
if (noMock
|
|
390
|
+
|| !executorName
|
|
391
|
+
|| executorSupportsSampleMocks(executorName)
|
|
392
|
+
|| warnedAutoMocklessExecutors.has(executorName))
|
|
393
|
+
return;
|
|
394
|
+
warnedAutoMocklessExecutors.add(executorName);
|
|
395
|
+
process.stderr.write(`[omk sample] 执行器「${executorName}」不支持工具调用拦截,`
|
|
396
|
+
+ '已自动切换为无 mocks 用例。\n');
|
|
397
|
+
}
|
|
398
|
+
export function sampleGenerationUsesMocks(executorName, noMock = false) {
|
|
399
|
+
if (noMock)
|
|
400
|
+
return false;
|
|
401
|
+
return executorName ? executorSupportsSampleMocks(executorName) : true;
|
|
402
|
+
}
|
|
370
403
|
/**
|
|
371
404
|
* 拼出送给 LLM 的 user prompt。抽出来便于单测验证 focus 是否真的注入了。
|
|
372
405
|
*
|
|
@@ -390,9 +423,12 @@ ${skillContent}
|
|
|
390
423
|
|
|
391
424
|
${countLine}直接输出 JSON 数组。${focusBlock}${noMockBlock}`;
|
|
392
425
|
}
|
|
393
|
-
export async function generateSamples({ skillContent, count, model, executorName, focus, noMock }) {
|
|
394
|
-
const executor = createExecutor(executorName);
|
|
395
|
-
const
|
|
426
|
+
export async function generateSamples({ skillContent, count, model, executorName, focus, noMock, executor: injectedExecutor, }) {
|
|
427
|
+
const executor = injectedExecutor ?? createExecutor(executorName);
|
|
428
|
+
const mockless = !sampleGenerationUsesMocks(executorName, noMock);
|
|
429
|
+
warnAutoMockless(executorName, noMock);
|
|
430
|
+
const prompt = buildSamplesPrompt({ skillContent, count, focus, noMock: mockless });
|
|
431
|
+
const system = generationSystemPrompt(mockless);
|
|
396
432
|
// 生成场景比单次 eval 调用更重(LLM 要思考结构 + 输出大段 JSON),
|
|
397
433
|
// 默认 120s 对长 skill + count >= 8 经常不够,这里用 5 分钟兜底。
|
|
398
434
|
// lean=true 关掉 agent 工具循环 / skill 发现 — 生成只需要纯文本,不需要 Bash / Read 等工具。
|
|
@@ -405,7 +441,7 @@ export async function generateSamples({ skillContent, count, model, executorName
|
|
|
405
441
|
const attemptPrompt = attempt === 1
|
|
406
442
|
? prompt
|
|
407
443
|
: `${prompt}\n\n上一次输出解析失败:${lastErr}\n请严格按 JSON 规范输出(字符串内部用「」全角引号),只输出数组,不要包含其他文字。`;
|
|
408
|
-
const result = await executor({ model, system
|
|
444
|
+
const result = await executor({ model, system, prompt: attemptPrompt, timeoutMs: 300_000, lean: true });
|
|
409
445
|
totalCost += result.costUSD || 0;
|
|
410
446
|
if (!result.ok) {
|
|
411
447
|
lastErr = result.error || 'unknown error';
|
|
@@ -435,12 +471,15 @@ export async function generateSamples({ skillContent, count, model, executorName
|
|
|
435
471
|
continue;
|
|
436
472
|
}
|
|
437
473
|
// 通过校验,跳出循环继续后续 sanitize
|
|
438
|
-
return await finalizeSamples(samples, totalCost,
|
|
474
|
+
return await finalizeSamples(samples, totalCost, {
|
|
475
|
+
skillContent,
|
|
476
|
+
mockless,
|
|
477
|
+
});
|
|
439
478
|
}
|
|
440
479
|
// 不可达 (循环里所有出口都 throw 或 return),保留是为了 TS 类型推断
|
|
441
480
|
throw new Error('unreachable');
|
|
442
481
|
}
|
|
443
|
-
async function finalizeSamples(samples, costUSD,
|
|
482
|
+
async function finalizeSamples(samples, costUSD, options) {
|
|
444
483
|
// Validate required fields + sanitize metadata enums *at generator boundary*
|
|
445
484
|
// (see sanitizeGeneratedSamples). skillContent is passed so the function can
|
|
446
485
|
// strip "脑补"-style fact assertions whose tool name has no literal mention
|
|
@@ -448,7 +487,10 @@ async function finalizeSamples(samples, costUSD, skillContent) {
|
|
|
448
487
|
// the data-security-review v1-v5 regen series (generator kept producing
|
|
449
488
|
// tool_input_contains "WebFetch:语雀URL" even after 7 prompt iterations,
|
|
450
489
|
// because LLM's "URL → fetch" training prior overrides instructional text).
|
|
451
|
-
const { stripped } = sanitizeGeneratedSamples(samples, {
|
|
490
|
+
const { stripped } = sanitizeGeneratedSamples(samples, {
|
|
491
|
+
...options,
|
|
492
|
+
migrateMocklessEnvironment: true,
|
|
493
|
+
});
|
|
452
494
|
if (stripped.length > 0) {
|
|
453
495
|
process.stderr.write(`[omk sample] LLM-output 含 ${stripped.length} 个非法元数据/断言字段,已剥离避免污染:\n - ${stripped.join('\n - ')}\n`);
|
|
454
496
|
}
|
|
@@ -492,7 +534,7 @@ export function stratifyTraceSignals(items) {
|
|
|
492
534
|
* production failures. The trace text feeds the *generator* only — never the
|
|
493
535
|
* judge prompt — so judge-prompt isolation is unaffected.
|
|
494
536
|
*/
|
|
495
|
-
export function buildSamplesFromTracesPrompt(items, count) {
|
|
537
|
+
export function buildSamplesFromTracesPrompt(items, count, options = {}) {
|
|
496
538
|
// 先按频次分层(合并重复 + 算占比 + 降序),让模型按「占比」分配配额,而非每信号一刀切。
|
|
497
539
|
const stratified = stratifyTraceSignals(items);
|
|
498
540
|
const sections = stratified.map((it, i) => {
|
|
@@ -515,13 +557,16 @@ export function buildSamplesFromTracesPrompt(items, count) {
|
|
|
515
557
|
const countLine = typeof count === 'number'
|
|
516
558
|
? `共生成约 ${count} 条评测用例,按各信号的「占比」分配配额(高频多、低频少),覆盖整体失败分布。`
|
|
517
559
|
: '按各信号「占比」分配:高频信号多生成、低频少生成,覆盖整体失败分布。';
|
|
560
|
+
const mocklessBlock = options.noMock
|
|
561
|
+
? '\n\n目标执行器不支持工具调用拦截:不要生成 mocks、mocksStrict、environment 或正向工具调用断言;把 trace 证据写入 context 和 rubric。'
|
|
562
|
+
: '';
|
|
518
563
|
return `${TRACE_GEN_INSTRUCTIONS}
|
|
519
564
|
|
|
520
565
|
## 观测到的失败信号(共 ${stratified.length} 个,已按出现频次降序)
|
|
521
566
|
|
|
522
567
|
${sections}
|
|
523
568
|
|
|
524
|
-
${countLine}直接输出 JSON
|
|
569
|
+
${countLine}直接输出 JSON 数组。${mocklessBlock}`;
|
|
525
570
|
}
|
|
526
571
|
/** Build a sanitize context string from trace evidence so finalizeSamples keeps
|
|
527
572
|
* tool-name assertions that reference tools actually seen in the traces (an empty
|
|
@@ -539,14 +584,17 @@ function traceSanitizeContext(items) {
|
|
|
539
584
|
* stamps `provenance: 'production-trace'`. Output is meant to land in a review draft,
|
|
540
585
|
* not the live dataset (the CLI enforces that).
|
|
541
586
|
*/
|
|
542
|
-
export async function generateSamplesFromTraces({ items, count, model, executorName, executor: injectedExecutor, }) {
|
|
587
|
+
export async function generateSamplesFromTraces({ items, count, model, executorName, noMock, executor: injectedExecutor, }) {
|
|
543
588
|
if (items.length === 0)
|
|
544
589
|
return { samples: [], costUSD: 0 };
|
|
545
590
|
if (!injectedExecutor && !executorName) {
|
|
546
591
|
throw new Error('executorName is required when no executor is injected');
|
|
547
592
|
}
|
|
548
593
|
const executor = injectedExecutor ?? createExecutor(executorName);
|
|
549
|
-
const
|
|
594
|
+
const mockless = !sampleGenerationUsesMocks(executorName, noMock);
|
|
595
|
+
warnAutoMockless(executorName, noMock);
|
|
596
|
+
const prompt = buildSamplesFromTracesPrompt(items, count, { noMock: mockless });
|
|
597
|
+
const system = generationSystemPrompt(mockless);
|
|
550
598
|
const sanitizeContext = traceSanitizeContext(items);
|
|
551
599
|
const PROVENANCE = 'production-trace';
|
|
552
600
|
const MAX_ATTEMPTS = 3;
|
|
@@ -556,7 +604,7 @@ export async function generateSamplesFromTraces({ items, count, model, executorN
|
|
|
556
604
|
const attemptPrompt = attempt === 1
|
|
557
605
|
? prompt
|
|
558
606
|
: `${prompt}\n\n上一次输出解析失败:${lastErr}\n请严格按 JSON 规范输出(字符串内部用「」全角引号),只输出数组,不要包含其他文字。`;
|
|
559
|
-
const result = await executor({ model, system
|
|
607
|
+
const result = await executor({ model, system, prompt: attemptPrompt, timeoutMs: 300_000, lean: true });
|
|
560
608
|
totalCost += result.costUSD || 0;
|
|
561
609
|
if (!result.ok) {
|
|
562
610
|
lastErr = result.error || 'unknown error';
|
|
@@ -593,7 +641,10 @@ export async function generateSamplesFromTraces({ items, count, model, executorN
|
|
|
593
641
|
// Stamp provenance before sanitize so it survives (it's a valid enum value).
|
|
594
642
|
for (const s of samples)
|
|
595
643
|
s.provenance = PROVENANCE;
|
|
596
|
-
return await finalizeSamples(samples, totalCost,
|
|
644
|
+
return await finalizeSamples(samples, totalCost, {
|
|
645
|
+
skillContent: sanitizeContext,
|
|
646
|
+
mockless,
|
|
647
|
+
});
|
|
597
648
|
}
|
|
598
649
|
throw new Error('unreachable');
|
|
599
650
|
}
|
|
@@ -656,6 +707,78 @@ const TEXT_VALUE_TYPES = new Set([
|
|
|
656
707
|
const TOOL_POSITIVE_TYPES = new Set([
|
|
657
708
|
'tool_input_contains', 'tool_output_contains', 'mock_hit',
|
|
658
709
|
]);
|
|
710
|
+
const MOCKLESS_POSITIVE_TOOL_TYPES = new Set([
|
|
711
|
+
'mock_hit',
|
|
712
|
+
'tools_called',
|
|
713
|
+
'tools_count_min',
|
|
714
|
+
'tool_input_contains',
|
|
715
|
+
'tool_output_contains',
|
|
716
|
+
]);
|
|
717
|
+
function stripMocklessPositiveToolAssertions(assertions, label, stripped) {
|
|
718
|
+
const kept = [];
|
|
719
|
+
for (const [index, assertion] of assertions.entries()) {
|
|
720
|
+
const assertionLabel = `${label}[${index}]`;
|
|
721
|
+
if (assertion?.type === 'assert-set' && Array.isArray(assertion.children)) {
|
|
722
|
+
const children = stripMocklessPositiveToolAssertions(assertion.children, `${assertionLabel}.children`, stripped);
|
|
723
|
+
if (children.length === 0) {
|
|
724
|
+
stripped.push(`${assertionLabel}.assert-set(无 mock 模式下没有可执行子断言)`);
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
727
|
+
assertion.children = children;
|
|
728
|
+
kept.push(assertion);
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
if (MOCKLESS_POSITIVE_TOOL_TYPES.has(assertion?.type)) {
|
|
732
|
+
stripped.push(`${assertionLabel}.${assertion.type}(目标执行器不支持 mocks,正向工具证据不可复现)`);
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
kept.push(assertion);
|
|
736
|
+
}
|
|
737
|
+
return kept;
|
|
738
|
+
}
|
|
739
|
+
function stripInvalidMockHitAssertions(assertions, mockKeys, label, stripped) {
|
|
740
|
+
const kept = [];
|
|
741
|
+
for (const [index, assertion] of assertions.entries()) {
|
|
742
|
+
const assertionLabel = `${label}[${index}]`;
|
|
743
|
+
if (assertion?.type === 'assert-set' && Array.isArray(assertion.children)) {
|
|
744
|
+
const children = stripInvalidMockHitAssertions(assertion.children, mockKeys, `${assertionLabel}.children`, stripped);
|
|
745
|
+
if (children.length === 0) {
|
|
746
|
+
stripped.push(`${assertionLabel}.assert-set(没有可执行子断言)`);
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
assertion.children = children;
|
|
750
|
+
kept.push(assertion);
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
if (assertion?.type === 'mock_hit'
|
|
754
|
+
&& typeof assertion.value === 'string'
|
|
755
|
+
&& !mockKeys.has(assertion.value)) {
|
|
756
|
+
stripped.push(`${assertionLabel}.mock_hit(未引用实际 mock:${assertion.value})`);
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
kept.push(assertion);
|
|
760
|
+
}
|
|
761
|
+
return kept;
|
|
762
|
+
}
|
|
763
|
+
function environmentAsPromptContext(environment) {
|
|
764
|
+
if (!environment || typeof environment !== 'object' || Array.isArray(environment)) {
|
|
765
|
+
return null;
|
|
766
|
+
}
|
|
767
|
+
const env = environment;
|
|
768
|
+
const lines = ['题设环境声明(仅作上下文,不会在 cwd 物化):'];
|
|
769
|
+
if (Array.isArray(env.cli_available)
|
|
770
|
+
&& env.cli_available.every((item) => typeof item === 'string' && item.length > 0)) {
|
|
771
|
+
lines.push(`- 可用 CLI:${env.cli_available.join('、')}`);
|
|
772
|
+
}
|
|
773
|
+
if (Array.isArray(env.files_available)
|
|
774
|
+
&& env.files_available.every((item) => typeof item === 'string' && item.length > 0)) {
|
|
775
|
+
lines.push(`- 题设引用路径(未物化):${env.files_available.join('、')}`);
|
|
776
|
+
}
|
|
777
|
+
if (typeof env.notes === 'string' && env.notes.trim()) {
|
|
778
|
+
lines.push(`- 说明:${env.notes.trim()}`);
|
|
779
|
+
}
|
|
780
|
+
return lines.length > 1 ? lines.join('\n') : null;
|
|
781
|
+
}
|
|
659
782
|
function isAsciiTokenLike(v) {
|
|
660
783
|
if (typeof v !== 'string')
|
|
661
784
|
return false;
|
|
@@ -719,6 +842,40 @@ export function sanitizeGeneratedSamples(samples, opts = {}) {
|
|
|
719
842
|
stripped.push(`samples[${i}].tripwire (${typeof s.tripwire})`);
|
|
720
843
|
delete s.tripwire;
|
|
721
844
|
}
|
|
845
|
+
if (opts.mockless) {
|
|
846
|
+
if (Array.isArray(s.mocks) && s.mocks.length > 0) {
|
|
847
|
+
stripped.push(`samples[${i}].mocks(目标执行器不支持工具调用拦截)`);
|
|
848
|
+
}
|
|
849
|
+
delete s.mocks;
|
|
850
|
+
if (s.mocksStrict !== undefined) {
|
|
851
|
+
stripped.push(`samples[${i}].mocksStrict(无 mocks)`);
|
|
852
|
+
}
|
|
853
|
+
delete s.mocksStrict;
|
|
854
|
+
if (s.environment !== undefined && !opts.preserveMocklessEnvironment) {
|
|
855
|
+
const environmentContext = opts.migrateMocklessEnvironment
|
|
856
|
+
? environmentAsPromptContext(s.environment)
|
|
857
|
+
: null;
|
|
858
|
+
if (environmentContext) {
|
|
859
|
+
const existingContext = typeof s.context === 'string' ? s.context.trim() : '';
|
|
860
|
+
s.context = existingContext
|
|
861
|
+
? `${existingContext}\n\n${environmentContext}`
|
|
862
|
+
: environmentContext;
|
|
863
|
+
stripped.push(`samples[${i}].environment(已迁移到题设 context,未物化)`);
|
|
864
|
+
}
|
|
865
|
+
else {
|
|
866
|
+
stripped.push(`samples[${i}].environment(未物化的环境声明不能充当 fixture)`);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
if (!opts.preserveMocklessEnvironment) {
|
|
870
|
+
delete s.environment;
|
|
871
|
+
}
|
|
872
|
+
if (Array.isArray(s.assertions)) {
|
|
873
|
+
s.assertions = stripMocklessPositiveToolAssertions(s.assertions, `samples[${i}].assertions`, stripped);
|
|
874
|
+
if (s.assertions.length === 0) {
|
|
875
|
+
delete s.assertions;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
722
879
|
// assertions 校验:loader 会拒掉两类无效断言 — 在 generator boundary 提前 strip,
|
|
723
880
|
// 避免落盘的 sample 跑不动:
|
|
724
881
|
// 1. tools_called / tools_not_called 的 values 必须非空
|
|
@@ -843,6 +1000,12 @@ export function sanitizeGeneratedSamples(samples, opts = {}) {
|
|
|
843
1000
|
delete s.mocks;
|
|
844
1001
|
}
|
|
845
1002
|
}
|
|
1003
|
+
if (Array.isArray(s.assertions)) {
|
|
1004
|
+
s.assertions = stripInvalidMockHitAssertions(s.assertions, sampleMockReferenceKeys(s.mocks), `samples[${i}].assertions`, stripped);
|
|
1005
|
+
if (s.assertions.length === 0) {
|
|
1006
|
+
delete s.assertions;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
846
1009
|
// mocksStrict 兜底:有 mocks 时强制 true。
|
|
847
1010
|
// SYSTEM_PROMPT 已要求 LLM 必填,但偶尔 LLM 漏填 — 在 generator boundary 修掉,
|
|
848
1011
|
// 避免运行时 mock 未命中透传到真 shell(报 mcporter not found 等噪声错误)。
|
|
@@ -34,6 +34,8 @@ export interface FixSamplesOptions {
|
|
|
34
34
|
}>;
|
|
35
35
|
model: string;
|
|
36
36
|
maxAttemptsPerSample?: number;
|
|
37
|
+
/** Target executor cannot intercept tool calls; strip mocks and dependent positive evidence. */
|
|
38
|
+
mockless?: boolean;
|
|
37
39
|
}
|
|
38
40
|
export interface FixSamplesResult {
|
|
39
41
|
samples: Record<string, unknown>[];
|
|
@@ -20,8 +20,17 @@ const FIX_SYSTEM_PROMPT = `你是一个评测用例修复专家。根据诊断
|
|
|
20
20
|
输出一个 **JSON 数组**,包含所有待修复 sample(修改过的和原样保留的都要包含)。
|
|
21
21
|
第一字符 \`[\`,最后 \`]\`,不要用 \`\`\`json\`\`\` 围栏,不要寒暄。
|
|
22
22
|
如果判断某条是 LLM 行为问题不需要改,也原样放进数组。`;
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
const MOCKLESS_FIX_OVERRIDE = `
|
|
24
|
+
|
|
25
|
+
目标执行器不支持工具调用拦截。不得新增或保留 mocks、mocksStrict、
|
|
26
|
+
mock_hit、tools_called、tools_count_min、tool_input_contains、tool_output_contains。
|
|
27
|
+
environment 仅表示题设上下文,不得当作已物化 fixture;负向安全断言可以保留。`;
|
|
28
|
+
function sanitizeFixedSamples(samples, skillContent, mockless) {
|
|
29
|
+
sanitizeGeneratedSamples(samples, {
|
|
30
|
+
skillContent,
|
|
31
|
+
mockless,
|
|
32
|
+
preserveMocklessEnvironment: true,
|
|
33
|
+
});
|
|
25
34
|
return samples;
|
|
26
35
|
}
|
|
27
36
|
const FIXABLE_SAMPLE_FIELDS = new Set([
|
|
@@ -97,7 +106,7 @@ function parseFixedSamples(text) {
|
|
|
97
106
|
return null;
|
|
98
107
|
}
|
|
99
108
|
export async function fixSamples(options) {
|
|
100
|
-
const { skillContent, samples, report, treatmentKey, executor, model, maxAttemptsPerSample = 2 } = options;
|
|
109
|
+
const { skillContent, samples, report, treatmentKey, executor, model, maxAttemptsPerSample = 2, mockless = false, } = options;
|
|
101
110
|
const sampleMap = new Map(samples.map((s) => [s.sample_id, s]));
|
|
102
111
|
// Collect all fixable samples into one batch
|
|
103
112
|
const fixContexts = [];
|
|
@@ -185,7 +194,12 @@ ${sampleSections}
|
|
|
185
194
|
let incurredCostUSD = 0;
|
|
186
195
|
let incurredCostReported = false;
|
|
187
196
|
try {
|
|
188
|
-
const result = await executor({
|
|
197
|
+
const result = await executor({
|
|
198
|
+
model,
|
|
199
|
+
system: mockless ? `${FIX_SYSTEM_PROMPT}${MOCKLESS_FIX_OVERRIDE}` : FIX_SYSTEM_PROMPT,
|
|
200
|
+
prompt,
|
|
201
|
+
timeoutMs: 300_000,
|
|
202
|
+
});
|
|
189
203
|
incurredCostUSD = result.costUSD;
|
|
190
204
|
incurredCostReported = result.costReported !== false;
|
|
191
205
|
if (!result.ok) {
|
|
@@ -226,7 +240,7 @@ ${sampleSections}
|
|
|
226
240
|
})),
|
|
227
241
|
};
|
|
228
242
|
}
|
|
229
|
-
const sanitizedFixedArr = sanitizeFixedSamples(fixedArr, skillContent);
|
|
243
|
+
const sanitizedFixedArr = sanitizeFixedSamples(fixedArr, skillContent, mockless);
|
|
230
244
|
const outOfScope = sanitizedFixedArr.find((fixed) => {
|
|
231
245
|
const sid = fixed.sample_id;
|
|
232
246
|
const original = sampleMap.get(sid);
|
|
@@ -274,7 +274,7 @@ async function runSampleFix(args, flags, lang) {
|
|
|
274
274
|
throw new CliExit(1);
|
|
275
275
|
}
|
|
276
276
|
process.stderr.write(lang === 'zh' ? `🔧 发现 ${sampleDesignCount} 条 sample_design 失败,开始修复...\n` : `🔧 Found ${sampleDesignCount} sample_design failure(s), fixing...\n`);
|
|
277
|
-
const { createExecutor } = await import('../../executors/index.js');
|
|
277
|
+
const { createExecutor, executorSupportsSampleMocks, } = await import('../../executors/index.js');
|
|
278
278
|
const exec = createExecutor(executorName);
|
|
279
279
|
const executorFn = async (opts) => {
|
|
280
280
|
const result = await exec({
|
|
@@ -298,6 +298,7 @@ async function runSampleFix(args, flags, lang) {
|
|
|
298
298
|
treatmentKey: treatmentName,
|
|
299
299
|
executor: executorFn,
|
|
300
300
|
model,
|
|
301
|
+
mockless: !executorSupportsSampleMocks(executorName),
|
|
301
302
|
});
|
|
302
303
|
let writtenFiles = [];
|
|
303
304
|
if (result.fixedCount > 0) {
|
|
@@ -363,7 +364,13 @@ export async function runSampleFromTraces(flags, lang) {
|
|
|
363
364
|
? `🔭 发现 ${items.length} 个${flags.skill ? ` ${flags.skill} 的` : ''}失败信号,正在生成评测用例草稿...\n`
|
|
364
365
|
: `🔭 Found ${items.length}${flags.skill ? ` ${flags.skill}` : ''} failure signal(s); generating regression-sample drafts...\n`);
|
|
365
366
|
try {
|
|
366
|
-
const { samples, costUSD } = await generateSamplesFromTraces({
|
|
367
|
+
const { samples, costUSD } = await generateSamplesFromTraces({
|
|
368
|
+
items,
|
|
369
|
+
count,
|
|
370
|
+
model,
|
|
371
|
+
executorName,
|
|
372
|
+
noMock: flags['no-mock'],
|
|
373
|
+
});
|
|
367
374
|
const cost = costUSD > 0 ? ` $${costUSD.toFixed(4)}` : '';
|
|
368
375
|
if (samples.length === 0) {
|
|
369
376
|
// The model conservatively skipped every signal (noise / unreproducible). That's a
|
|
@@ -663,8 +670,8 @@ export default class Sample extends BaseCommand {
|
|
|
663
670
|
}),
|
|
664
671
|
'no-mock': Flags.boolean({
|
|
665
672
|
description: bilingual({
|
|
666
|
-
zh: '不生成 mocks
|
|
667
|
-
en: 'Skip
|
|
673
|
+
zh: '不生成 mocks。执行器不支持工具拦截时会自动启用,避免产生必然失败的 mock_hit。',
|
|
674
|
+
en: 'Skip mocks. Automatically enabled when the executor cannot intercept tools, preventing impossible mock_hit assertions.',
|
|
668
675
|
}),
|
|
669
676
|
default: false,
|
|
670
677
|
}),
|
|
@@ -25,7 +25,14 @@ const SYSTEM_ENV_VARS = new Set([
|
|
|
25
25
|
'TMPDIR', 'EDITOR', 'VISUAL', 'HOSTNAME', 'LOGNAME', 'DISPLAY',
|
|
26
26
|
'XDG_CONFIG_HOME', 'XDG_DATA_HOME', 'XDG_CACHE_HOME',
|
|
27
27
|
'NODE_ENV', 'NODE_PATH', 'NODE_OPTIONS', 'NPM_CONFIG_PREFIX',
|
|
28
|
+
// Agent Skills resolves this placeholder to the active skill directory.
|
|
29
|
+
// It is not a user-provided environment prerequisite.
|
|
30
|
+
'SKILL_ROOT',
|
|
28
31
|
]);
|
|
32
|
+
// A shell probe that explicitly tolerates failure is discovering the target
|
|
33
|
+
// project shape. Candidate paths on that line are not hard dependencies of the
|
|
34
|
+
// skill itself.
|
|
35
|
+
const OPTIONAL_PROBE_LINE_REGEX = /(?:2>\s*\/dev\/null|\|\|\s*(?:true|:)(?:\s|$))/;
|
|
29
36
|
function extractFromText(text) {
|
|
30
37
|
const tools = new Set();
|
|
31
38
|
const files = new Set();
|
|
@@ -42,25 +49,30 @@ function extractFromText(text) {
|
|
|
42
49
|
tools.add(cmd);
|
|
43
50
|
}
|
|
44
51
|
}
|
|
45
|
-
// File paths
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (path.startsWith('http') || path.startsWith('node_modules') || /^\d/.test(path))
|
|
52
|
+
// File paths. Keep line context so optional discovery commands do not turn
|
|
53
|
+
// every candidate project entry point into a fatal preflight requirement.
|
|
54
|
+
for (const line of text.split(/\r?\n/)) {
|
|
55
|
+
if (OPTIONAL_PROBE_LINE_REGEX.test(line))
|
|
50
56
|
continue;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
for (const match of line.matchAll(FILE_PATH_REGEX)) {
|
|
58
|
+
const path = match[1];
|
|
59
|
+
// Skip paths that look like URLs, package names, or version strings
|
|
60
|
+
if (path.startsWith('http') || path.startsWith('node_modules') || /^\d/.test(path))
|
|
61
|
+
continue;
|
|
62
|
+
// Skip very short paths that are likely not real files
|
|
63
|
+
if (path.length < 5)
|
|
64
|
+
continue;
|
|
65
|
+
// Skip extension-mention patterns(`.d.ts` / `.tsx` 这种以点开头的"扩展名讨论"
|
|
66
|
+
// 不是真路径,SKILL.md 里"查看 .d.ts 文件"会被误识别)
|
|
67
|
+
if (path.startsWith('.'))
|
|
68
|
+
continue;
|
|
69
|
+
// Skip bare filenames without a directory segment(`index.ts` / `package.json`
|
|
70
|
+
// 这种通用文件名几乎都是示例性提及,真依赖会带路径段。要声明 bare 文件
|
|
71
|
+
// 走显式 requires)
|
|
72
|
+
if (!path.includes('/'))
|
|
73
|
+
continue;
|
|
74
|
+
files.add(path);
|
|
75
|
+
}
|
|
64
76
|
}
|
|
65
77
|
// Environment variables
|
|
66
78
|
for (const match of text.matchAll(ENV_VAR_REGEX)) {
|
|
@@ -258,8 +258,17 @@ export async function executeTasks({ tasks, executor, executorName, model, noJud
|
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
260
|
let factCheck;
|
|
261
|
-
if (execResult.ok && execResult.output
|
|
262
|
-
|
|
261
|
+
if (execResult.ok && execResult.output) {
|
|
262
|
+
const sharedEvidence = {
|
|
263
|
+
...(task._sample.context && { context: task._sample.context }),
|
|
264
|
+
...(task._sample.environment?.files_available?.length && {
|
|
265
|
+
declaredFiles: task._sample.environment.files_available,
|
|
266
|
+
}),
|
|
267
|
+
// Resolve exactly like the executor's cwd. samplesBaseDir is for bundle
|
|
268
|
+
// assets such as mocks, not for changing Sample.cwd path semantics.
|
|
269
|
+
...(task._sample.cwd && { cwd: resolve(task._sample.cwd) }),
|
|
270
|
+
};
|
|
271
|
+
factCheck = checkFacts(execResult.output, sharedEvidence);
|
|
263
272
|
}
|
|
264
273
|
const sampleResults = ownRecordValue(results, task.sample_id)
|
|
265
274
|
?? setOwnRecordValue(results, task.sample_id, {});
|
|
@@ -14,11 +14,24 @@ export interface FactCheckResult {
|
|
|
14
14
|
totalCount: number;
|
|
15
15
|
verifiedRate: number;
|
|
16
16
|
}
|
|
17
|
+
export interface FactCheckEvidence {
|
|
18
|
+
/** Shared sample fixture root. Arm-specific execution directories must not be used. */
|
|
19
|
+
cwd?: string;
|
|
20
|
+
/** Facts supplied to both arms in the sample context. */
|
|
21
|
+
context?: string;
|
|
22
|
+
/** Declarative fixture paths from sample.environment.files_available. */
|
|
23
|
+
declaredFiles?: string[];
|
|
24
|
+
}
|
|
17
25
|
/**
|
|
18
26
|
* Extract file path claims from agent output text.
|
|
19
27
|
*/
|
|
20
28
|
export declare function extractPathClaims(output: string): string[];
|
|
21
29
|
/**
|
|
22
|
-
* Check facts
|
|
30
|
+
* Check file-path facts against sample-level evidence shared by every arm.
|
|
31
|
+
*
|
|
32
|
+
* A string keeps the legacy direct-filesystem API for callers outside the
|
|
33
|
+
* evaluation pipeline. The pipeline passes structured evidence and deliberately
|
|
34
|
+
* excludes each artifact's execution cwd, because that directory is not a
|
|
35
|
+
* comparable source of truth across control and treatment arms.
|
|
23
36
|
*/
|
|
24
|
-
export declare function checkFacts(output: string,
|
|
37
|
+
export declare function checkFacts(output: string, evidence: string | FactCheckEvidence): FactCheckResult;
|