@catheadowl/dsh-eval 0.1.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 +6 -0
- package/README.md +59 -55
- package/README.zh.md +129 -0
- package/bin/dsh-eval.mjs +335 -335
- package/bin/dsh-review.mjs +166 -154
- package/docs/README.md +6 -2
- package/docs/cross-turn.md +66 -0
- package/docs/disablerows.md +1 -1
- package/docs/experimental.md +35 -0
- package/docs/host-wiring.md +3 -3
- package/docs/known-issues.md +9 -1
- package/docs/matchers.md +28 -2
- package/docs/review.md +13 -10
- package/docs/rowconfig.md +39 -0
- package/docs/runner-api.md +42 -0
- package/package.json +19 -3
- package/src/adapters/dsh/review.mjs +210 -192
- package/src/assertions.mjs +499 -389
- package/src/cli.mjs +8 -8
- package/src/config.mjs +1 -1
- package/src/discovery.mjs +190 -115
- package/src/driver/multi-turn-driver.mjs +163 -0
- package/src/experiment/review.mjs +118 -118
- package/src/experimental.mjs +39 -0
- package/src/index.mjs +38 -49
- package/src/mock/mock-adapter.mjs +73 -73
- package/src/mock/script.mjs +49 -49
- package/src/overlay.mjs +109 -0
- package/src/report.mjs +1 -1
- package/src/review-report.mjs +14 -1
- package/src/runner.mjs +236 -373
- package/src/sandbox.mjs +262 -0
- package/src/tool-validation.mjs +77 -77
- package/src/trace.mjs +293 -218
- package/src/adapters/dsh/index.mjs +0 -7
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 契约、intent case
|
|
2
|
+
description: dsh-eval 文档索引——安装与宿主接线、review、matcher 全集、disableRows/rowConfig/followups 契约、intent case 规约、报告结构与已知问题的路由表
|
|
3
3
|
---
|
|
4
4
|
|
|
5
5
|
# dsh-eval · docs index
|
|
@@ -7,9 +7,13 @@ 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
|
+
| [rowconfig.md](rowconfig.md) | `rowConfig` 行 config 覆写契约(整段替换语义、形状限制、与 disableRows 分工) |
|
|
14
|
+
| [cross-turn.md](cross-turn.md) | `followups` 跨轮异步驱动契约(driver 行换装、子 agent settle 等待、mock 单 cursor 编排、有界重派断言) |
|
|
13
15
|
| [intent-cases.md](intent-cases.md) | real 意图 case 规约:何时写、断言面、守卫、CI 语义 |
|
|
14
16
|
| [report.md](report.md) | 机器可读报告(`--format json` / `--report`)结构 |
|
|
17
|
+
| [runner-api.md](runner-api.md) | 程序化 runner API:`runEvalCase` options 契约、EvalRunResult 字段、跨档取 `cliPath` |
|
|
15
18
|
| [known-issues.md](known-issues.md) | 已知问题与规避 |
|
|
19
|
+
| [experimental.md](experimental.md) | `experimental` 子路径符号清单(逃生面,无兼容承诺) |
|
|
@@ -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/disablerows.md
CHANGED
|
@@ -15,7 +15,7 @@ eval 的临时工作区通常**不是 git 仓库**——doc-link 类 gate 在其
|
|
|
15
15
|
## 契约
|
|
16
16
|
|
|
17
17
|
- 默认**不声明** = 所选 profile 装载的插件照常运行(gate 交互 case——如断言 gate steer 的 `userMessageTextIncludes`——依赖此默认)。
|
|
18
|
-
- 声明 `disableRows: ['gates']`(case 级或 config 级)= 本次 run 禁用 gates 插件行(行 id 权威:`@catheadowl/dsh-extras` 包的 `cordis.patch.yml` `- id: gates
|
|
18
|
+
- 声明 `disableRows: ['gates']`(case 级或 config 级)= 本次 run 禁用 gates 插件行(行 id 权威:`@catheadowl/dsh-extras` 包的 `cordis.patch.yml` `- id: gates`——其他 dsh 插件包),终态违规不再触发 splice,`finalText` 保持「脚本终步文本」的确定性语义。禁用其他插件行同理,行 id 以该插件包的 patch 声明为准。
|
|
19
19
|
- gate 交互 case 在默认禁用的包里声明 `disableRows: []` 显式恢复装载。
|
|
20
20
|
- 不依赖插件开关的断言出口:`assistantTextIncludes`(断言脚本台词出现过,不要求是最终文本)。终态干净时仍应优先 `finalText*`。
|
|
21
21
|
- per-gate 白名单(如只关某个 gate)暂不支持:per-gate disable 需要 gate 框架侧先提供 config 面。
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: experimental 子路径符号清单——机制原语(沙箱/overlay/trace、review 执行器、CLI 解析链)的逃生面;无兼容承诺,随 minor 变更
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# experimental 子路径(逃生面)
|
|
6
|
+
|
|
7
|
+
<!-- experimental-tier-warning -->
|
|
8
|
+
|
|
9
|
+
> **⚠️ 无兼容承诺**:本入口的全部符号可在任意 minor 版本变更、移动或移除,不另行弃用周期。case 作者请使用包根入口(断言 DSL、`textStep` / `toolCallStep`、`defineReviewExperiment`、`runEvalCase`);只有自建执行面 / ad-hoc 诊断脚本才应 import 本入口,并自行承担跟进成本。
|
|
10
|
+
|
|
11
|
+
```js
|
|
12
|
+
import { resolveDshCliChain } from '@catheadowl/dsh-eval/experimental'
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 符号清单
|
|
16
|
+
|
|
17
|
+
| 符号 | 用途 |
|
|
18
|
+
|---|---|
|
|
19
|
+
| `resolveDshCliChain` | 现代三段式 dsh CLI 解析链(`--repo` 旗标 → 解析层 node_modules → config `repo`),返回 `{ cli, repo, source }`;解析失败同步抛错(指引见 [host-wiring.md](host-wiring.md)) |
|
|
20
|
+
| `stageProfileStore` | 把真实 profile store junction 感知地暂存进沙箱 home(沙箱机制) |
|
|
21
|
+
| `buildOverlayYaml` | 由片段拼装 dsh overlay YAML(整段发射器) |
|
|
22
|
+
| `overlayDisableRows` | 生成 `disabled: true` 的行禁用 overlay 片段 |
|
|
23
|
+
| `parseSessionLog` | 解析一条未压缩 JSONL session artifact 为 `{ header, events }` |
|
|
24
|
+
| `buildTrace` | 把 session 事件投影为 matcher 使用的 trace 对象 |
|
|
25
|
+
| `loadTraceDir` | 从 run 目录装载并解析 trace(无日志时返回 `undefined`) |
|
|
26
|
+
| `executeReviewExperiment` | 用给定 executor 执行抽象 review 实验 |
|
|
27
|
+
| `materializeReviewExperiment` | 把实验定义物化为产物目录 |
|
|
28
|
+
| `renderObservationSections` | 标准 observation renderer(自定义 executor 用) |
|
|
29
|
+
| `OBSERVATIONS_PLACEHOLDER` | prompt 中的观测占位符常量(必须恰好出现一次) |
|
|
30
|
+
| `createDshHeadlessReviewExecutor` | 构造 dsh headless review executor |
|
|
31
|
+
| `runDshReviewExperiment` | 端到端跑一个 dsh review 实验 |
|
|
32
|
+
| `validateToolBoundary` | 校验 trace 满足 turn-close 工具边界契约 |
|
|
33
|
+
| `renderToolBoundaryEvidence` | 渲染边界校验的机器可读证据 |
|
|
34
|
+
|
|
35
|
+
新公开能力先进本入口;稳定后经明确决策才升入包根入口(升入即接受 semver 义务)。
|
package/docs/host-wiring.md
CHANGED
|
@@ -45,7 +45,7 @@ node -e "console.log(require('./node_modules/@deepseek-ai/dsh-llm/package.json')
|
|
|
45
45
|
dsh CLI 的定位按以下顺序,先中先得:
|
|
46
46
|
|
|
47
47
|
1. **显式 flag**:`--repo <host-checkout>`(检出须已构建,`apps/cli/lib/bin.js` 存在);
|
|
48
|
-
2. **解析层**:`node_modules/@deepseek-ai/dsh/lib/bin.js`(开发态由 junction
|
|
48
|
+
2. **解析层**:`node_modules/@deepseek-ai/dsh/lib/bin.js`(开发态由 junction 指到宿主检出,junction 失效时须重建为指回本地宿主检出;消费态由安装树提供);
|
|
49
49
|
3. **config `repo` 键**:legacy,已从各包入库 config 退役。
|
|
50
50
|
|
|
51
51
|
三层全缺时 fail-loud(报错含占位符修法指引)。自诊断:
|
|
@@ -54,13 +54,13 @@ dsh CLI 的定位按以下顺序,先中先得:
|
|
|
54
54
|
node -e "console.log(require('fs').existsSync('node_modules/@deepseek-ai/dsh/lib/bin.js'))"
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
`false` = 解析层缺 CLI
|
|
57
|
+
`false` = 解析层缺 CLI:先把上述 junction 重建为指向宿主检出;仍 `false` 则宿主检出未构建(先构建宿主)。这类 junction 维护是机器相关的开发环境事务,不入库,由各开发环境自行承接(同上文 peer 接线的 gitignore 纪律)。behavior 与 review 的真实运行都从定位到的 CLI spawn dsh 本体。
|
|
58
58
|
|
|
59
59
|
## 环境面:profile 与插件安装
|
|
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,12 +1,27 @@
|
|
|
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
|
|
6
6
|
|
|
7
7
|
全部从包根导入:`import { toolCalled, … } from '@catheadowl/dsh-eval'`。
|
|
8
8
|
|
|
9
|
-
断言对象是 dsh session 事件投影(`
|
|
9
|
+
断言对象是 dsh session 事件投影(`EvalTrace`),不只是「模型产出」:`requestHeaders` 投影模型被挂载的工具与 system prompt(输入面),`userMessages` 投影 user-role 的模型可见输入面(任务 prompt、插件 steer、注入上下文)——这让 mock 能断言插件的**驱动级 steer**,而不只断工具选择或最终文本。
|
|
10
|
+
|
|
11
|
+
## EvalTrace 形状(谓词与 `result.trace` 共用)
|
|
12
|
+
|
|
13
|
+
| 字段 | 形状 |
|
|
14
|
+
|---|---|
|
|
15
|
+
| `toolCalls` | `{ seq, turn, step, callId, name, arguments, parsedArguments }[]`(`arguments` 原文,`parsedArguments` 已 JSON 解析) |
|
|
16
|
+
| `toolResults` | `{ seq, turn, step, callId, text, error?, isError? }[]`(与 `toolCalls` 按 `callId` 配对) |
|
|
17
|
+
| `assistantTexts` | `string[]` 非空组装 assistant 文本,按日志序 |
|
|
18
|
+
| `finalText` | 最后一个组装 assistant 文本(无则 `''`) |
|
|
19
|
+
| `userMessages` | `{ seq, source, text }[]`(`source` 原样透传:任务 prompt `{ kind: 'user' }`,插件 steer `{ kind: 'plugin', plugin }`) |
|
|
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 文本(无则 `''` = 派发了但没答) |
|
|
22
|
+
| `sessions` / `sessionId` | 原始解析结果 `{ header, events }[]` 与主 session id |
|
|
23
|
+
|
|
24
|
+
`runEvalCase` 返回的 `result.trace` 即此形状(无 session 日志时为 `undefined`;字段语义见 [runner-api.md](runner-api.md))。
|
|
10
25
|
|
|
11
26
|
## 工具面
|
|
12
27
|
|
|
@@ -29,6 +44,17 @@ description: trace matcher 与 mock helper 全集——工具面/文本面/输
|
|
|
29
44
|
- `toolMounted(name)`:工具出现在某个 request/header 的挂载列表;
|
|
30
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' }`)。
|
|
31
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
|
+
|
|
32
58
|
## Mock script helpers
|
|
33
59
|
|
|
34
60
|
- `toolCallStep(name, args)`:一步「模型调工具」,结束于 tool-calls;
|