@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/src/overlay.mjs
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overlay (cordis patch) serialization — the ONLY hand-rolled YAML emitter
|
|
3
|
+
* in the package. Both the behavior runner (buildOverlayYaml) and the dsh
|
|
4
|
+
* review adapter (overlayDisableRows for its tool-less overlay) generate
|
|
5
|
+
* per-run overlay files; sharing one emitter keeps quoting rules and
|
|
6
|
+
* row-patch syntax (`- id: <row>` / `disabled: true`) identical everywhere.
|
|
7
|
+
*
|
|
8
|
+
* YAML strategy: JSON double-quoted strings are valid YAML scalars, so the
|
|
9
|
+
* emitters lean on JSON.stringify — zero dependencies, identical quoting
|
|
10
|
+
* across scalar and array leaves.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { join } from 'node:path'
|
|
14
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
15
|
+
|
|
16
|
+
/** This framework's root directory (the eval package dir). */
|
|
17
|
+
const FRAMEWORK_ROOT = fileURLToPath(new URL('..', import.meta.url))
|
|
18
|
+
|
|
19
|
+
/** The scripted mock adapter plugin, referenced from generated overlays. */
|
|
20
|
+
const MOCK_ADAPTER_PATH = join(FRAMEWORK_ROOT, 'src', 'mock', 'mock-adapter.mjs')
|
|
21
|
+
|
|
22
|
+
/** The multi-turn driver plugin, mounted when a case declares `followups`. */
|
|
23
|
+
const MULTI_TURN_DRIVER_PATH = join(FRAMEWORK_ROOT, 'src', 'driver', 'multi-turn-driver.mjs')
|
|
24
|
+
|
|
25
|
+
/** JSON double-quoted strings are valid YAML scalars — enough for this emitter. */
|
|
26
|
+
function yamlScalar(value) {
|
|
27
|
+
if (typeof value === 'boolean' || typeof value === 'number') return String(value)
|
|
28
|
+
return JSON.stringify(String(value))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* One rowConfig leaf: a scalar, or an array of scalars emitted as a YAML flow
|
|
33
|
+
* sequence (a JSON array is valid YAML flow syntax, and keeps quoting rules
|
|
34
|
+
* identical to `yamlScalar`).
|
|
35
|
+
*/
|
|
36
|
+
function yamlConfigValue(value) {
|
|
37
|
+
if (Array.isArray(value)) return JSON.stringify(value.map(item => typeof item === 'string' ? item : String(item)))
|
|
38
|
+
return yamlScalar(value)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Serialize `- id: <row> / disabled: true` patches for every row id — the
|
|
43
|
+
* cross-layer row-disable mechanism shared by case `disableRows` and the
|
|
44
|
+
* review adapter's host-tool lockdown.
|
|
45
|
+
* @param {string[]} rowIds - loader row ids to disable.
|
|
46
|
+
* @returns {string} overlay entries (each line-terminated).
|
|
47
|
+
*/
|
|
48
|
+
export function overlayDisableRows(rowIds) {
|
|
49
|
+
return rowIds.map(rowId => `- id: ${yamlScalar(rowId)}\n disabled: true\n`).join('')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Serialize the per-run overlay patch list for one eval case.
|
|
54
|
+
* @param {object} parts - overlay ingredients (see runEvalCase):
|
|
55
|
+
* `sessionsRoot` (required), optional `persona`, `disableRows`,
|
|
56
|
+
* `rowConfig`, and `mock` (mount the scripted adapter + re-point the
|
|
57
|
+
* default model).
|
|
58
|
+
* @returns {string} the overlay file text.
|
|
59
|
+
*/
|
|
60
|
+
export function buildOverlayYaml(parts) {
|
|
61
|
+
const lines = []
|
|
62
|
+
lines.push('- id: session-persistence-jsonl')
|
|
63
|
+
lines.push(' config:')
|
|
64
|
+
lines.push(` root: ${yamlScalar(parts.sessionsRoot)}`)
|
|
65
|
+
lines.push(' packChunks: false')
|
|
66
|
+
lines.push(' compression: none')
|
|
67
|
+
if (parts.persona !== undefined) {
|
|
68
|
+
lines.push('- id: system-prompt')
|
|
69
|
+
lines.push(' config:')
|
|
70
|
+
lines.push(` persona: ${yamlScalar(parts.persona)}`)
|
|
71
|
+
}
|
|
72
|
+
if (parts.disableRows !== undefined && parts.disableRows.length > 0) {
|
|
73
|
+
lines.push(overlayDisableRows(parts.disableRows).trimEnd())
|
|
74
|
+
}
|
|
75
|
+
for (const [rowId, config] of Object.entries(parts.rowConfig ?? {})) {
|
|
76
|
+
// Whole-replace semantics: these config keys REPLACE the row's config
|
|
77
|
+
// (cordis patch layer), so the emitter adds to a fresh `- id:` entry —
|
|
78
|
+
// restating keys is the declaring case's responsibility.
|
|
79
|
+
lines.push(`- id: ${yamlScalar(rowId)}`)
|
|
80
|
+
lines.push(' config:')
|
|
81
|
+
for (const [key, value] of Object.entries(config)) {
|
|
82
|
+
lines.push(` ${key}: ${yamlConfigValue(value)}`)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (parts.mock) {
|
|
86
|
+
lines.push('- id: agent-default-model')
|
|
87
|
+
lines.push(' config:')
|
|
88
|
+
lines.push(' provider: eval-mock')
|
|
89
|
+
lines.push(' model: eval-mock')
|
|
90
|
+
// The title generator also calls the default provider and would consume
|
|
91
|
+
// script steps; deterministic runs own every model call themselves.
|
|
92
|
+
lines.push('- id: session-title-llm')
|
|
93
|
+
lines.push(' disabled: true')
|
|
94
|
+
lines.push('- insert:')
|
|
95
|
+
lines.push(' - id: eval-mock-llm')
|
|
96
|
+
lines.push(` name: ${yamlScalar(pathToFileURL(MOCK_ADAPTER_PATH).href)}`)
|
|
97
|
+
}
|
|
98
|
+
if (parts.followups !== undefined) {
|
|
99
|
+
// Cross-turn driving replaces the one-shot headless runner: it exits at
|
|
100
|
+
// the FIRST idle and aborts every in-process background subagent at
|
|
101
|
+
// teardown, so fire-and-forget children need the driver's longer lifetime.
|
|
102
|
+
lines.push('- id: headless-runner')
|
|
103
|
+
lines.push(' disabled: true')
|
|
104
|
+
lines.push('- insert:')
|
|
105
|
+
lines.push(' - id: eval-multi-turn-driver')
|
|
106
|
+
lines.push(` name: ${yamlScalar(pathToFileURL(MULTI_TURN_DRIVER_PATH).href)}`)
|
|
107
|
+
}
|
|
108
|
+
return `${lines.join('\n')}\n`
|
|
109
|
+
}
|
package/src/report.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Machine-readable run reports for the behavior CLI
|
|
2
|
+
* Machine-readable run reports for the behavior CLI.
|
|
3
3
|
*
|
|
4
4
|
* One report covers ONE `dsh-eval run` invocation: the selection summary,
|
|
5
5
|
* per-case outcomes, and the invocation's environment anchors (profile /
|
package/src/review-report.mjs
CHANGED
|
@@ -68,7 +68,20 @@ export function renderReviewReport(parts) {
|
|
|
68
68
|
if (attempt.ok) {
|
|
69
69
|
lines.push(`### run ${attempt.index} — ok`)
|
|
70
70
|
lines.push('')
|
|
71
|
-
|
|
71
|
+
// The answer, not the raw final message: adapters derive an
|
|
72
|
+
// answer that survives tail interactions (gate splices); stdout
|
|
73
|
+
// is the fallback for executors without trace-derived answers.
|
|
74
|
+
lines.push('```text', (attempt.result?.answer ?? attempt.result?.stdout ?? '').trimEnd(), '```')
|
|
75
|
+
// Traceability: the grader can always recover the full conversation
|
|
76
|
+
// from the persisted per-run artifacts. The divergence note matches
|
|
77
|
+
// the bin's write predicate exactly (both fields defined and
|
|
78
|
+
// differing) — a stdout-less executor writes no run-N.stdout.txt,
|
|
79
|
+
// so it must not be cited either.
|
|
80
|
+
const diverged = attempt.result?.answer !== undefined
|
|
81
|
+
&& attempt.result?.stdout !== undefined
|
|
82
|
+
&& attempt.result.answer !== attempt.result.stdout
|
|
83
|
+
lines.push('')
|
|
84
|
+
lines.push(`- transcript: \`run-${attempt.index}.stderr.txt\`${diverged ? ` (answer differs from the final message — see \`run-${attempt.index}.stdout.txt\`)` : ''}`)
|
|
72
85
|
} else {
|
|
73
86
|
lines.push(`### run ${attempt.index} — FAIL`)
|
|
74
87
|
lines.push('')
|