@catheadowl/dsh-eval 0.2.0 → 0.2.1
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/CHANGELOG.md +57 -0
- package/README.i18n.yaml +2 -2
- package/README.md +2 -5
- package/README.zh.md +2 -5
- package/bin/dsh-eval.mjs +335 -335
- package/bin/dsh-review.mjs +166 -154
- package/docs/README.md +3 -2
- package/docs/cross-turn.md +66 -0
- package/docs/host-wiring.md +1 -1
- package/docs/known-issues.md +9 -1
- package/docs/matchers.md +13 -1
- package/docs/review.md +12 -9
- package/package.json +7 -1
- package/src/adapters/dsh/review.mjs +70 -17
- package/src/assertions.mjs +499 -389
- package/src/discovery.mjs +190 -166
- package/src/driver/multi-turn-driver.mjs +163 -0
- package/src/experiment/review.mjs +118 -118
- package/src/index.mjs +4 -0
- package/src/mock/mock-adapter.mjs +73 -73
- package/src/mock/script.mjs +49 -49
- package/src/overlay.mjs +13 -0
- package/src/review-report.mjs +14 -1
- package/src/runner.mjs +21 -1
- package/src/sandbox.mjs +62 -1
- package/src/tool-validation.mjs +77 -77
- package/src/trace.mjs +293 -218
package/bin/dsh-review.mjs
CHANGED
|
@@ -1,154 +1,166 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Execute model-independent `*.review.mjs` experiments through dsh headless.
|
|
4
|
-
*
|
|
5
|
-
* Dry-run materializes live observations without touching dsh. Real runs write
|
|
6
|
-
* the shared task plus each independent review answer beside the experiment:
|
|
7
|
-
* `.runs/<experiment id>/`.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
11
|
-
import { dirname, join, resolve } from 'node:path'
|
|
12
|
-
import { pathToFileURL } from 'node:url'
|
|
13
|
-
import { materializeReviewExperiment } from '../src/experiment/review.mjs'
|
|
14
|
-
import { runDshReviewExperiment } from '../src/adapters/dsh/review.mjs'
|
|
15
|
-
import { discoverFiles } from '../src/discovery.mjs'
|
|
16
|
-
import { loadEvalConfig } from '../src/config.mjs'
|
|
17
|
-
import { resolveDshCliChain } from '../src/cli.mjs'
|
|
18
|
-
import { renderReviewReport } from '../src/review-report.mjs'
|
|
19
|
-
|
|
20
|
-
function usage(error) {
|
|
21
|
-
const message = [
|
|
22
|
-
'usage: dsh-review [--dry-run] [--runs N] [--profile NAME (default: headless) --repo DIR] [--timeout MS] <*.review.mjs or directories...>',
|
|
23
|
-
' --profile/--repo may come from a dsh-eval.config.mjs found upward from cwd; flags override it.',
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
process.
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (arg === '--
|
|
38
|
-
if (arg === '--
|
|
39
|
-
if (arg === '--
|
|
40
|
-
if (arg === '
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
const
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Execute model-independent `*.review.mjs` experiments through dsh headless.
|
|
4
|
+
*
|
|
5
|
+
* Dry-run materializes live observations without touching dsh. Real runs write
|
|
6
|
+
* the shared task plus each independent review answer beside the experiment:
|
|
7
|
+
* `.runs/<experiment id>/`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
11
|
+
import { dirname, join, resolve } from 'node:path'
|
|
12
|
+
import { pathToFileURL } from 'node:url'
|
|
13
|
+
import { materializeReviewExperiment } from '../src/experiment/review.mjs'
|
|
14
|
+
import { runDshReviewExperiment } from '../src/adapters/dsh/review.mjs'
|
|
15
|
+
import { discoverFiles } from '../src/discovery.mjs'
|
|
16
|
+
import { loadEvalConfig } from '../src/config.mjs'
|
|
17
|
+
import { resolveDshCliChain } from '../src/cli.mjs'
|
|
18
|
+
import { renderReviewReport } from '../src/review-report.mjs'
|
|
19
|
+
|
|
20
|
+
function usage(error) {
|
|
21
|
+
const message = [
|
|
22
|
+
'usage: dsh-review [--dry-run] [--runs N] [--profile NAME (default: headless) --repo DIR] [--timeout MS] [--keep-plugin-rows] <*.review.mjs or directories...>',
|
|
23
|
+
' --profile/--repo may come from a dsh-eval.config.mjs found upward from cwd; flags override it.',
|
|
24
|
+
' blank environment by default: every out-of-tree plugin row in the staged profile is disabled;',
|
|
25
|
+
' --keep-plugin-rows opts back into the host profile plugin face (e.g. reviewing a plugin\'s own gates).',
|
|
26
|
+
].join('\n')
|
|
27
|
+
if (error) process.stderr.write(`${error}\n${message}\n`)
|
|
28
|
+
else process.stdout.write(`${message}\n`)
|
|
29
|
+
process.exit(error ? 2 : 0)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parseArgs(argv) {
|
|
33
|
+
const options = { dryRun: false, runs: undefined, timeoutMs: undefined, profile: undefined, repo: undefined, keepPluginRows: false }
|
|
34
|
+
const paths = []
|
|
35
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
36
|
+
const arg = argv[index]
|
|
37
|
+
if (arg === '--dry-run') { options.dryRun = true; continue }
|
|
38
|
+
if (arg === '--runs') { options.runs = Number(argv[++index]); continue }
|
|
39
|
+
if (arg === '--profile') { options.profile = argv[++index]; continue }
|
|
40
|
+
if (arg === '--repo') { options.repo = argv[++index]; continue }
|
|
41
|
+
if (arg === '--timeout') { options.timeoutMs = Number(argv[++index]); continue }
|
|
42
|
+
if (arg === '--keep-plugin-rows') { options.keepPluginRows = true; continue }
|
|
43
|
+
if (arg === '-h' || arg === '--help') usage()
|
|
44
|
+
paths.push(arg)
|
|
45
|
+
}
|
|
46
|
+
if (paths.length === 0) usage('error: at least one review experiment path is required')
|
|
47
|
+
if (options.runs !== undefined && (!Number.isInteger(options.runs) || options.runs < 1)) usage('error: --runs must be a positive integer')
|
|
48
|
+
if (options.timeoutMs !== undefined && (!Number.isInteger(options.timeoutMs) || options.timeoutMs < 1)) usage('error: --timeout must be a positive integer')
|
|
49
|
+
return { options, paths }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function discover(path) {
|
|
53
|
+
return discoverFiles(path, '.review.mjs')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function loadExperiment(file) {
|
|
57
|
+
const module = await import(pathToFileURL(file).href)
|
|
58
|
+
const experiment = module.default
|
|
59
|
+
if (experiment?.kind !== 'review') {
|
|
60
|
+
throw new Error(`${file}: default export must come from defineReviewExperiment(...)`)
|
|
61
|
+
}
|
|
62
|
+
return { ...experiment, __file: file }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function artifactDir(experiment) {
|
|
66
|
+
return join(dirname(experiment.__file), '.runs', experiment.id)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function writeMaterialized(experiment, materialized, extra = {}, reviewResult = undefined) {
|
|
70
|
+
const output = artifactDir(experiment)
|
|
71
|
+
mkdirSync(output, { recursive: true })
|
|
72
|
+
writeFileSync(join(output, 'task.txt'), materialized.task, 'utf8')
|
|
73
|
+
writeFileSync(join(output, 'observations.md'), materialized.observations, 'utf8')
|
|
74
|
+
writeFileSync(join(output, 'run.json'), JSON.stringify({
|
|
75
|
+
experimentId: experiment.id,
|
|
76
|
+
summary: experiment.summary,
|
|
77
|
+
rubric: String(experiment.rubric),
|
|
78
|
+
...extra,
|
|
79
|
+
}, null, 2), 'utf8')
|
|
80
|
+
writeFileSync(join(output, 'review-report.md'), renderReviewReport({
|
|
81
|
+
experiment,
|
|
82
|
+
result: reviewResult,
|
|
83
|
+
observations: materialized.observations,
|
|
84
|
+
adapter: extra.adapter,
|
|
85
|
+
profile: extra.profile,
|
|
86
|
+
}), 'utf8')
|
|
87
|
+
return output
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const { options, paths } = parseArgs(process.argv.slice(2))
|
|
91
|
+
|
|
92
|
+
// Config merge: flags win over a `dsh-eval.config.mjs` found
|
|
93
|
+
// upward from cwd; profile falls back to the sterile default `headless`.
|
|
94
|
+
const { config } = await loadEvalConfig(process.cwd())
|
|
95
|
+
const profile = options.profile ?? config.profile ?? 'headless'
|
|
96
|
+
// CLI resolution (C6): `--repo` flag > resolution layer (node_modules) >
|
|
97
|
+
// config repo key (legacy). Dry-run never boots the CLI, so resolve lazily.
|
|
98
|
+
let cli = { cliPath: undefined, repoDir: undefined }
|
|
99
|
+
if (!options.dryRun) {
|
|
100
|
+
try {
|
|
101
|
+
const resolved = resolveDshCliChain({ repoFlag: options.repo, configRepo: config.repo })
|
|
102
|
+
cli = { cliPath: resolved.cli, repoDir: resolved.repo }
|
|
103
|
+
} catch (error) {
|
|
104
|
+
usage(`error: ${error.message}`)
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
for (const path of paths) {
|
|
109
|
+
if (!existsSync(resolve(path))) usage(`error: no such experiment path: ${path}`)
|
|
110
|
+
}
|
|
111
|
+
const files = paths.flatMap(path => discover(path)).sort()
|
|
112
|
+
if (files.length === 0) usage('error: no *.review.mjs experiment files found')
|
|
113
|
+
|
|
114
|
+
let failures = 0
|
|
115
|
+
for (const file of files) {
|
|
116
|
+
let experiment
|
|
117
|
+
try {
|
|
118
|
+
experiment = await loadExperiment(file)
|
|
119
|
+
if (options.dryRun) {
|
|
120
|
+
const materialized = await materializeReviewExperiment(experiment)
|
|
121
|
+
const output = writeMaterialized(experiment, materialized, { adapter: null, dryRun: true })
|
|
122
|
+
process.stdout.write(`DRY ${experiment.id}: ${output}\n`)
|
|
123
|
+
continue
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
process.stdout.write(`RUN ${experiment.id} (${options.runs ?? experiment.defaultRuns} reviews)...\n`)
|
|
127
|
+
const result = await runDshReviewExperiment(experiment, {
|
|
128
|
+
profile,
|
|
129
|
+
cliPath: cli.cliPath,
|
|
130
|
+
dshRepoDir: cli.repoDir,
|
|
131
|
+
runs: options.runs,
|
|
132
|
+
timeoutMs: options.timeoutMs,
|
|
133
|
+
keepPluginRows: options.keepPluginRows,
|
|
134
|
+
})
|
|
135
|
+
const output = writeMaterialized(experiment, result, {
|
|
136
|
+
adapter: 'dsh-headless',
|
|
137
|
+
profile,
|
|
138
|
+
runs: result.runs,
|
|
139
|
+
}, result)
|
|
140
|
+
for (const attempt of result.attempts) {
|
|
141
|
+
const payload = attempt.result ?? {}
|
|
142
|
+
// run-N.txt is the reviewer's ANSWER (trace-derived, splice-proof),
|
|
143
|
+
// falling back to stdout for executors/trace-less runs; the raw final
|
|
144
|
+
// message stays in run-N.stdout.txt when it differs from the answer,
|
|
145
|
+
// the full session transcript in run-N.stderr.txt.
|
|
146
|
+
if (payload.answer !== undefined) writeFileSync(join(output, `run-${attempt.index}.txt`), payload.answer, 'utf8')
|
|
147
|
+
else if (payload.stdout !== undefined) writeFileSync(join(output, `run-${attempt.index}.txt`), payload.stdout, 'utf8')
|
|
148
|
+
if (payload.stdout !== undefined && payload.answer !== undefined && payload.stdout !== payload.answer) {
|
|
149
|
+
writeFileSync(join(output, `run-${attempt.index}.stdout.txt`), payload.stdout, 'utf8')
|
|
150
|
+
}
|
|
151
|
+
if (payload.stderr) writeFileSync(join(output, `run-${attempt.index}.stderr.txt`), payload.stderr, 'utf8')
|
|
152
|
+
if (payload.toolBoundaryEvidence) writeFileSync(join(output, `run-${attempt.index}.tool-boundary-evidence.json`), payload.toolBoundaryEvidence, 'utf8')
|
|
153
|
+
if (!attempt.ok) {
|
|
154
|
+
failures += 1
|
|
155
|
+
writeFileSync(join(output, `run-${attempt.index}.error.txt`), attempt.error, 'utf8')
|
|
156
|
+
process.stderr.write(`FAIL ${experiment.id} run ${attempt.index}: ${attempt.error}\n`)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (result.attempts.every(attempt => attempt.ok)) process.stdout.write(`DONE ${experiment.id}: ${output}\n`)
|
|
160
|
+
} catch (error) {
|
|
161
|
+
failures += 1
|
|
162
|
+
process.stderr.write(`FAIL ${file}: ${error.message}\n`)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
process.exit(failures === 0 ? 0 : 1)
|
package/docs/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: dsh-eval 文档索引——安装与宿主接线、review、matcher 全集、disableRows/rowConfig 契约、intent case 规约、报告结构与已知问题的路由表
|
|
2
|
+
description: dsh-eval 文档索引——安装与宿主接线、review、matcher 全集、disableRows/rowConfig/followups 契约、intent case 规约、报告结构与已知问题的路由表
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
# dsh-eval · docs index
|
|
@@ -7,10 +7,11 @@ description: dsh-eval 文档索引——安装与宿主接线、review、matcher
|
|
|
7
7
|
| 文档 | 主题 |
|
|
8
8
|
|---|---|
|
|
9
9
|
| [host-wiring.md](host-wiring.md) | 安装与宿主接线:peer 依赖(dsh-llm)、构建 CLI、profile、凭证、spawn 要求 |
|
|
10
|
-
| [review.md](review.md) | comprehension review
|
|
10
|
+
| [review.md](review.md) | comprehension review:实验定义、空白环境 reviewer、产物清单、六条评审规则 |
|
|
11
11
|
| [matchers.md](matchers.md) | trace matcher 与 mock helper 全集 |
|
|
12
12
|
| [disablerows.md](disablerows.md) | `disableRows` 与 turn-close 门禁边界契约 |
|
|
13
13
|
| [rowconfig.md](rowconfig.md) | `rowConfig` 行 config 覆写契约(整段替换语义、形状限制、与 disableRows 分工) |
|
|
14
|
+
| [cross-turn.md](cross-turn.md) | `followups` 跨轮异步驱动契约(driver 行换装、子 agent settle 等待、mock 单 cursor 编排、有界重派断言) |
|
|
14
15
|
| [intent-cases.md](intent-cases.md) | real 意图 case 规约:何时写、断言面、守卫、CI 语义 |
|
|
15
16
|
| [report.md](report.md) | 机器可读报告(`--format json` / `--report`)结构 |
|
|
16
17
|
| [runner-api.md](runner-api.md) | 程序化 runner API:`runEvalCase` options 契约、EvalRunResult 字段、跨档取 `cliPath` |
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: followups 跨轮驱动契约——case 声明 followups 后 overlay 换装 eval 多轮 driver、后台子 agent 的 settle 等待语义、mock 脚本跨轮共享 cursor 的编排规则与 bounded-redispatch 断言面
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# followups:跨轮异步驱动
|
|
6
|
+
|
|
7
|
+
单轮 behavior case 的驱动模型覆盖不了 defer 自愈闭环:「turn 收尾派发 fork 子 agent → 子 agent 落盘修复 → **下一轮** stop 重扫转绿(或每轮一次有界重派)」。case 声明 `followups` 即启用跨轮驱动。
|
|
8
|
+
|
|
9
|
+
## 为什么是 driver 行而不是 CLI resume
|
|
10
|
+
|
|
11
|
+
实测定案(「跨轮异步修复驱动」FR 调研,见开发仓的 eval 状态板记录):
|
|
12
|
+
|
|
13
|
+
- headless runner 在主 agent 首次 idle 即退出,**进程退出会把 in-process 后台子 agent 当场 abort**(fire-and-forget 子 agent 存活窗口 = driver 的生命周期);
|
|
14
|
+
- dsh CLI 无 resume 表面(one task per invocation;resume 只在 cordis config 层)。
|
|
15
|
+
|
|
16
|
+
因此 `followups` 的实现是 overlay 换装:disable `headless-runner` 行 + insert 本包的 `eval-multi-turn-driver`(`src/driver/multi-turn-driver.mjs`,file:// 挂载,与 mock adapter 同机制)。driver 语义:
|
|
17
|
+
|
|
18
|
+
1. 以 case `task` 驱动 turn 1,等主 agent idle;
|
|
19
|
+
2. 每个 followup 之前,**等待后台子 agent settle**(全局 `session/event` 投影:subagent 会话自 `turn/start` 起 pending、`turn/end` 止;空集后再过 250ms 静默宽限才放行;上限 `settleTimeoutMs`,默认 60s,超时报错退出);
|
|
20
|
+
3. 提交 followup(普通 user message)驱动下一轮;
|
|
21
|
+
4. 最后一轮收尾后**同样等待一次 settle**——最后一轮的 turn close 也会派发(defer gate 每次失败 stop 都派 fixer),不等就会在退出时静默截断它们;「已派发 ⇒ 可观测结局」对每一轮成立;
|
|
22
|
+
5. flush、按 headless 同款输出契约退出(最后一条非空 assistant 文本到 stdout;最后 turn `completed` → exit 0)。
|
|
23
|
+
|
|
24
|
+
## case 声明
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
export default {
|
|
28
|
+
id: 'gates-mock-defer-self-heal',
|
|
29
|
+
mode: 'mock',
|
|
30
|
+
disableRows: [], // gate 交互 case 需显式装载 gates 行
|
|
31
|
+
task: '…turn 1 任务…',
|
|
32
|
+
followups: ['rescan now'], // 每项 = 一个额外驱动轮
|
|
33
|
+
settleTimeoutMs: 30_000, // 可选;等后台子 agent 的上限
|
|
34
|
+
script: { steps: [/* 跨轮共享 cursor,见下 */] },
|
|
35
|
+
expect: [/* matchers */],
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
校验:`followups` 必须是非空 string[];`settleTimeoutMs`(可选)必须是正有限数。仅声明 `settleTimeoutMs` 而无 `followups` 无效(不校验、不生效)。
|
|
40
|
+
|
|
41
|
+
## mock 脚本编排:单 cursor 跨轮共享
|
|
42
|
+
|
|
43
|
+
mock adapter 是单实例、单 cursor:**每次模型调用按序吃一步,不区分轮次与 会话**。跨轮 case 的 steps 是全运行编排——主 turn 步骤、子 agent 步骤、后续轮步骤交错排列:
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
steps: [
|
|
47
|
+
textStep('turn 1 done'), // 主 turn 1 收尾 → turn close 派发 fixer
|
|
48
|
+
textStep('fixer child answer'), // fixer 子 agent 的首个(唯一)模型调用
|
|
49
|
+
textStep('turn 2 done'), // followup 轮的收尾
|
|
50
|
+
]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
注意:turn-close blocking gate 的 splice 反馈步骤(见 [disablerows.md](disablerows.md))同样消耗 cursor——编排跨轮脚本时先数清每轮会被 splice 几步。
|
|
54
|
+
|
|
55
|
+
## 断言面
|
|
56
|
+
|
|
57
|
+
- 派发与完成:`subagentDispatched` / `subagentCompleted`(见 [matchers.md](matchers.md) 派发面);
|
|
58
|
+
- **有界重派节律**:`subagentDispatchCount(matcher, expected)`——精确断言匹配 label 的派发总数(「每轮恰一次、N 轮共 N 次、不更多」);
|
|
59
|
+
- 跨轮重扫:`userMessages` 投影带全部轮次的 `user/message`,`userMessageTextIncludes` / `userMessageTextExcludes` 按 `source` 断言某轮是否被 gate steer(例如 turn 2 重扫转绿 → 无新的 gates steer)。
|
|
60
|
+
|
|
61
|
+
## 边界
|
|
62
|
+
|
|
63
|
+
- **settle 宽限是启发式,不是 barrier**:250ms 静默宽限可被「慢派发」击穿——若某插件的 turn-close 派发在主 agent idle 后 >250ms 才发出子 `turn/start`,driver 会空集放行、followup 先行(mock 面表现为脚本错步、通常 loud;real 面表现为轮次与子 agent 并发交叉)。宿主没有「派发完成」事件可用,这是对不存在信号的保守近似。
|
|
64
|
+
- **时间预算叠加**:driver 的 settle 等待(默认每轮上限 60s)叠加在 case `timeoutMs`(runner 的 spawn 超时)之内。轮数 × settle 上限若超过 `timeoutMs`,先到的是 spawn 超时(结果 `timedOut`)而非 settle 报错——多轮 case 应按轮数放大 `timeoutMs`。
|
|
65
|
+
- `subagentCompleted` 的「完成」以子会话产出非空 assistant 文本为准;被早夭 abort 的子日志可能不含 `subagent/descriptor`(label 投影为空)——见 matchers.md 派发面的早夭边界。要断言「每个派发都跑完」(而非任一跑完),用 `subagentCompletedCount`。
|
|
66
|
+
- real 模式下 `followups` 同样可用(driver 不依赖 mock),但轮次内容非确定——real 意图 case 通常不需要它。
|
package/docs/host-wiring.md
CHANGED
|
@@ -60,7 +60,7 @@ node -e "console.log(require('fs').existsSync('node_modules/@deepseek-ai/dsh/lib
|
|
|
60
60
|
|
|
61
61
|
- 被测插件须已装进所选 profile:`dsh plugin --profile <profile> add <插件目录>`;
|
|
62
62
|
- 每条 behavior case 在隔离的临时 `DSH_HOME` 与 workspace 中启动 dsh,通过 `--patch` 把 session JSONL 定向到本次 run——不污染真实 profile store;
|
|
63
|
-
- review
|
|
63
|
+
- review 默认空白环境:任意 profile 均可,树外插件行由 overlay 枚举禁用(详见 [review.md](review.md))。
|
|
64
64
|
|
|
65
65
|
## 凭证
|
|
66
66
|
|
package/docs/known-issues.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: 已知问题——real case 在 staged home 的 REQUEST_EXTENSION 失败(嫌疑 plugin-package-inventory-deepseek
|
|
2
|
+
description: 已知问题——real case 在 staged home 的 REQUEST_EXTENSION 失败(嫌疑 plugin-package-inventory-deepseek)、DSH file sandbox 下 dsh-review spawn EPERM 及 disableRows 兜底
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
# 已知问题
|
|
@@ -13,3 +13,11 @@ description: 已知问题——real case 在 staged home 的 REQUEST_EXTENSION
|
|
|
13
13
|
**框架侧兜底**:case 或 config 声明 `disableRows: ['plugin-package-inventory-deepseek']` 按行禁用该插件(机制见 [disablerows.md](disablerows.md))。
|
|
14
14
|
|
|
15
15
|
**追踪**:上游修复落地即删本条(不留僵尸条目)。
|
|
16
|
+
|
|
17
|
+
## DSH file sandbox 下 `dsh-review` spawn headless 子进程 `EPERM`(未解)
|
|
18
|
+
|
|
19
|
+
**症状**:在 DSH file sandbox(受限运行面)内运行 `dsh-review`,首次 spawn headless 子进程即报 `spawn EPERM`,escalated retry 后可运行。
|
|
20
|
+
|
|
21
|
+
**定性**:沙箱运行面约束,非框架缺陷——沙箱拒绝 spawn 时换宿主侧终端或升级运行面即可(与本包维护规则「沙箱拒绝 spawn 时在宿主侧终端跑,不绕测试」同族)。登记供沙箱内调用方知晓。
|
|
22
|
+
|
|
23
|
+
**追踪**:开发侧状态板按名登记为「2026-09-06 · dsh-review 沙箱 spawn EPERM 已知约束」(按名对齐);运行面行为变化时同删。
|
package/docs/matchers.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: trace matcher 与 mock helper
|
|
2
|
+
description: trace matcher 与 mock helper 全集——工具面/文本面/输入面/派发面断言语义(toolCalled 到 subagentCompletedCount)与 toolCallStep/textStep 脚本构件
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
# Trace matchers 与 mock helpers
|
|
@@ -18,6 +18,7 @@ description: trace matcher 与 mock helper 全集——工具面/文本面/输
|
|
|
18
18
|
| `finalText` | 最后一个组装 assistant 文本(无则 `''`) |
|
|
19
19
|
| `userMessages` | `{ seq, source, text }[]`(`source` 原样透传:任务 prompt `{ kind: 'user' }`,插件 steer `{ kind: 'plugin', plugin }`) |
|
|
20
20
|
| `requestHeaders` | `{ seq, reason, system, toolNames }[]`(组装后 system prompt + 挂载工具名) |
|
|
21
|
+
| `subagentChildren` | `{ sessionId, parentSession, delegationDepth, label, mode, provider, assistantTexts, finalText }[]`——每个子 agent 独立 session 日志一条;身份(label/mode/provider)取子日志首条 version-3 的 `subagent/descriptor` 事件(镜像宿主 `foldSubagentDescriptor` 的首条权威语义),`finalText` 是子会话自己的最后一条非空 assistant 文本(无则 `''` = 派发了但没答) |
|
|
21
22
|
| `sessions` / `sessionId` | 原始解析结果 `{ header, events }[]` 与主 session id |
|
|
22
23
|
|
|
23
24
|
`runEvalCase` 返回的 `result.trace` 即此形状(无 session 日志时为 `undefined`;字段语义见 [runner-api.md](runner-api.md))。
|
|
@@ -43,6 +44,17 @@ description: trace matcher 与 mock helper 全集——工具面/文本面/输
|
|
|
43
44
|
- `toolMounted(name)`:工具出现在某个 request/header 的挂载列表;
|
|
44
45
|
- `userMessageTextIncludes(source, substring)` / `userMessageTextExcludes(source, substring)`:按 `source` 过滤的 `user/message` 文本含/不含子串。`source` 用字符串/RegExp 匹配 `plugin` 名(如 steer 生产方),或谓词取整个 `source`——steer 在持久化日志里没有专名事件(`agent.steer()` 落为 `user/message`),区分靠 `source`(插件 steer 为 `{ kind: 'plugin', plugin: '<id>' }`,任务 prompt 为 `{ kind: 'user' }`)。
|
|
45
46
|
|
|
47
|
+
## 派发面(子 agent)
|
|
48
|
+
|
|
49
|
+
主 session 日志不含派发事件(宿主不向父 session 写),但子 agent 的独立 session 日志与主日志同 persistence root,随 run 一并被收集——派发面投影即来自这些子日志:
|
|
50
|
+
|
|
51
|
+
- `subagentDispatched(label)`:至少一个子 agent 以匹配的 label 派发。`label` 用字符串/RegExp 匹配子会话的 `subagent/descriptor` label(如 `gates:fix:<gate>`、前缀 `/^gates:fix:/`),或谓词取整个子记录(可按 `mode`/`provider`/`delegationDepth` 匹配);
|
|
52
|
+
- `subagentCompleted(label)`:匹配的子 agent 产出了答案——其自身日志含至少一条非空 assistant 文本(不区分中止/正常收束:日志层无 subagent 完成事件,产出过文本即算)。只派发未应答(子日志存在但无产出)不通过。
|
|
53
|
+
- `subagentDispatchCount(label, expected)`:匹配 label 的派发**总数**恰为 `expected`——有界重派节律断言(「每轮恰一次、不更多」),配合跨轮驱动([cross-turn.md](cross-turn.md))。
|
|
54
|
+
- `subagentCompletedCount(label, expected)`:匹配 label 且**跑完**(产出非空 assistant 文本)的子 agent 恰为 `expected` 个。`subagentCompleted` 任一跑完即过;本 matcher 钉死每个派发的结局——「已派发 ⇒ 可观测结局」的跨轮 case 里,任一被截断的子 agent 都判负。
|
|
55
|
+
|
|
56
|
+
边界:子会话产物(独立 JSONL)经 `subagentChildren` 记录进入断言面(身份 + 子自身文本);子会话内部的工具调用**不**并入主投影的 `toolCalls`/`toolResults`(那属于主会话行为面),需要时经 `sessions` 原始日志自行投影。
|
|
57
|
+
|
|
46
58
|
## Mock script helpers
|
|
47
59
|
|
|
48
60
|
- `toolCallStep(name, args)`:一步「模型调工具」,结束于 tool-calls;
|
package/docs/review.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: comprehension review 指南——defineReviewExperiment
|
|
2
|
+
description: comprehension review 指南——defineReviewExperiment 实验定义、空白环境(默认禁树外插件行)盲评运行、.runs 产物与 review-report 判读模板、六条评审规则
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
# Comprehension review
|
|
@@ -34,7 +34,7 @@ export default defineReviewExperiment({
|
|
|
34
34
|
})
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
抽象层公开 `defineReviewExperiment`(稳定根入口)。实验执行与渲染原语——`materializeReviewExperiment`、`executeReviewExperiment`、标准 observation renderer(`renderObservationSections`;prompt 里的观测占位符常量是 `OBSERVATIONS_PLACEHOLDER`,必须恰好出现一次)——在 `@catheadowl/dsh-eval/experimental` 逃生面(见 [experimental.md](experimental.md),无兼容承诺)。自定义 executor 只需实现 `(task, context) => result`;接入其他 agent runtime
|
|
37
|
+
抽象层公开 `defineReviewExperiment`(稳定根入口)。实验执行与渲染原语——`materializeReviewExperiment`、`executeReviewExperiment`、标准 observation renderer(`renderObservationSections`;prompt 里的观测占位符常量是 `OBSERVATIONS_PLACEHOLDER`,必须恰好出现一次)——在 `@catheadowl/dsh-eval/experimental` 逃生面(见 [experimental.md](experimental.md),无兼容承诺)。自定义 executor 只需实现 `(task, context) => result`;接入其他 agent runtime 不需要改试验定义。结果契约上,报告消费 `result.answer`(对任务的回答;未提供时回落 `result.stdout`),`result.stderr` 落盘为每轮 transcript——自定义 executor 提供这两者即可获得同等的产物与报告行为。
|
|
38
38
|
|
|
39
39
|
## CLI
|
|
40
40
|
|
|
@@ -43,19 +43,21 @@ export default defineReviewExperiment({
|
|
|
43
43
|
dsh-review --dry-run <experiment file or directory>
|
|
44
44
|
|
|
45
45
|
dsh-review \
|
|
46
|
-
--profile <
|
|
46
|
+
--profile <profile> \
|
|
47
47
|
--repo <deepseek-harness checkout> \
|
|
48
|
-
[--runs 5] [--timeout 300000] \
|
|
48
|
+
[--runs 5] [--timeout 300000] [--keep-plugin-rows] \
|
|
49
49
|
<experiment file or directory>
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
`--profile`/`--repo` 可来自 `dsh-eval.config.mjs`(见 README),flags 覆盖 config。
|
|
53
53
|
|
|
54
|
-
##
|
|
54
|
+
## 空白环境(默认)与工具边界
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
真实运行的 reviewer 会话默认在**空白环境**启动:适配器先照常暂存所选 profile,再枚举它组合出的**全部树外插件行**(`package.json` 的 `dsh.profile.bundles` 中非 `@deepseek-ai/*` 的 bundle 各自 patch 文件里的行,加上 profile 自有 `cordis.patch.yml` 的行),在 `--patch` overlay 里逐行禁用——宿主 profile 装了什么 gates/插件都与 reviewer 无关,可复现性不再依赖「本机 profile 恰好干净」。白名单保留 reviewer 起不来就无测可言的接线行(`agent-default-model`、`session-title-llm`、`system-prompt`、`session-persistence-jsonl`);宿主模板工具行(`tool-fs`、shell、web、subagent 等)由静态清单继续禁用,cwd 指向空临时目录——reviewer 只能从物化的观测文本推理。运行后解析 session trace 的 `request/header` 事件做**工具边界校验**:发现任何非预期工具即视为 adapter failure(证据写入 `.runs/<id>/run-N.tool-boundary-evidence.json`)。
|
|
57
57
|
|
|
58
|
-
|
|
58
|
+
**刻意复用宿主插件面**(例如要评审某插件自己的 gate 行为):加 `--keep-plugin-rows`——跳过树外行枚举,仅保留静态工具禁用,宿主 gates 恢复运行。
|
|
59
|
+
|
|
60
|
+
> 注意:无 `id` 的组合条目对 id 定位的禁用天然不可见(宿主 loader 语义),本包的树外 bundle 生态均为带 id 行形态;发现无 id 树外行时以工具边界校验 fail-loud 兜底。白名单是**按行名**无条件保留——若某树外 bundle 刻意以白名单名(如 `system-prompt`)insert 自己的行,该行不会被禁(威胁模型是本机自己的 profile,非对抗面);此类泄漏同样由工具边界校验兜底。
|
|
59
61
|
|
|
60
62
|
## 产物
|
|
61
63
|
|
|
@@ -63,9 +65,10 @@ dsh-review \
|
|
|
63
65
|
|
|
64
66
|
- `observations.md`:本次实时物化的可见证据;
|
|
65
67
|
- `task.txt`:实际发给每位 reviewer 的完整任务;
|
|
66
|
-
- `run-N.txt
|
|
68
|
+
- `run-N.txt`:该轮 reviewer 的**答案**——trace 推导(首条插件注入消息之前的最后一条 assistant 文本,抗回合尾部劫持),无 trace 时回落到 stdout 的最终消息;劫持发生时原始最终消息另存 `run-N.stdout.txt`;
|
|
69
|
+
- `run-N.stderr.txt` / error:完整会话转录(判读回溯的 transcript 指针,报告每轮引用)/ 失败原因;
|
|
67
70
|
- `run.json`:experiment、rubric、adapter、profile 与 run 数;
|
|
68
|
-
- `review-report.md`:判读报告骨架——机器字段自动填(experiment/adapter/ profile/runs、observations 指纹、rubric 位置、每轮 reviewer
|
|
71
|
+
- `review-report.md`:判读报告骨架——机器字段自动填(experiment/adapter/ profile/runs、observations 指纹、rubric 位置、每轮 reviewer **答案**与 transcript 指针),三个 **人工判读栏目**留白待填:intentional design 命中项、新 red flag、下一步(改输出 / 改 rubric / 改 behavior case / 不处理)。刻意不做自动评分——review 层的价值在人工判断,报告只把判断物化成可归档、可对比的工程证据(dry-run 也会生成,runs 记 0)。
|
|
69
72
|
|
|
70
73
|
## 六条评审规则
|
|
71
74
|
|
package/package.json
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@catheadowl/dsh-eval",
|
|
3
3
|
"description": "Agent eval framework over dsh headless runs: case runner, session-trace assertions, and a scripted mock-LLM layer for plugin intent tests.",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.1",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/CatheadOwl/dsh-eval.git"
|
|
9
|
+
},
|
|
6
10
|
"type": "module",
|
|
11
|
+
"packageManager": "pnpm@11.7.0",
|
|
7
12
|
"main": "src/index.mjs",
|
|
8
13
|
"exports": {
|
|
9
14
|
".": "./src/index.mjs",
|
|
@@ -28,6 +33,7 @@
|
|
|
28
33
|
"docs/",
|
|
29
34
|
"README.md",
|
|
30
35
|
"README.zh.md",
|
|
36
|
+
"CHANGELOG.md",
|
|
31
37
|
"LICENSE"
|
|
32
38
|
],
|
|
33
39
|
"scripts": {
|