dsh-session-eval 0.2.3 → 0.3.0
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 +16 -2
- package/README.md +20 -1
- package/README.zh.md +20 -1
- package/lib/index.d.ts +37 -8
- package/lib/index.js +217 -59
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
## 0.3.0 — 2026-10-23
|
|
2
|
+
|
|
3
|
+
Feature release: configurable grade bands (#1) and the history time window (#2).
|
|
4
|
+
|
|
5
|
+
- New: a `gradeBands` config block overrides the default thresholds for any dimension, per-profile — `reliability` in error-rate percent, `reask` in corrections per 10 user turns, `toolLoad` as `[lowC, lowB, highC, highD]` calls per turn. Invalid config (unknown keys, wrong arity, non-numeric, negative, non-ascending) fails fast at plugin load with a readable error; defaults are unchanged when nothing is configured.
|
|
6
|
+
- New: `/eval-history --since 24h|7d|30d` grades only sessions whose latest activity falls inside the window — filtered by last event time, not creation time. An empty window prints a clear message; without the flag the behavior is exactly as before.
|
|
7
|
+
|
|
8
|
+
## 0.2.4 — 2026-10-11
|
|
9
|
+
|
|
10
|
+
Feature release: eval-history report files.
|
|
11
|
+
|
|
12
|
+
- New: `/eval-history --out PATH[.md|.json]` writes the history report to a Markdown or JSON file instead of only echoing it (contributed by @varshith84 in #4, merged as 117f918 after a maintainer rebase).
|
|
13
|
+
|
|
14
|
+
# Changelog
|
|
15
|
+
|
|
16
|
+
|
|
1
17
|
## 0.2.3 — 2026-10-03
|
|
2
18
|
|
|
3
19
|
Docs polish round (no runtime changes).
|
|
@@ -7,8 +23,6 @@ Docs polish round (no runtime changes).
|
|
|
7
23
|
- Community section; bilingual issue templates (`.github/ISSUE_TEMPLATE/`).
|
|
8
24
|
- English/Chinese README sections kept in sync.
|
|
9
25
|
|
|
10
|
-
# Changelog
|
|
11
|
-
|
|
12
26
|
## 0.2.2 — 2026-10-02
|
|
13
27
|
|
|
14
28
|
Contributor-first docs release.
|
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ All three read through the same trusted `ctx.sessionQuery` seam, so any persiste
|
|
|
65
65
|
|
|
66
66
|
- **Local dev**: `npm ci && npm run typecheck && npm test && npm run bundle` (Node 20 or 22).
|
|
67
67
|
- **Start in the source**: [`src/grade.ts`](src/grade.ts) (thresholds, grade cards), [`src/metrics.ts`](src/metrics.ts) (metric extraction), [`src/trend.ts`](src/trend.ts) (`/eval-history` trend logic). See [CONTRIBUTING.md](CONTRIBUTING.md) for the source map and first-PR suggestions.
|
|
68
|
-
- **Open gaps**: [#1](https://github.com/kittimzhe/dsh-session-eval/issues/1) (configurable thresholds)
|
|
68
|
+
- **Open gaps**: [#1](https://github.com/kittimzhe/dsh-session-eval/issues/1) (configurable thresholds) and [#2](https://github.com/kittimzhe/dsh-session-eval/issues/2) (`/eval-history --since`) shipped in v0.3.0 — browse [issues labeled `good first issue`](https://github.com/kittimzhe/dsh-session-eval/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22).
|
|
69
69
|
- **Rules**: behavior changes need tests; doc changes must update `README.md` and `README.zh.md` in sync; version releases belong to the maintainer. Details: [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
70
70
|
|
|
71
71
|
## How it differs from benchmark-style evaluation
|
|
@@ -90,9 +90,28 @@ Both are useful; they don't compete. Benchmarks test what you *predicted*; retro
|
|
|
90
90
|
| `/eval-diff … --json` | Machine-readable trend report |
|
|
91
91
|
| `/eval-history [N]` | Grade the last N sessions in this workspace and show the trend |
|
|
92
92
|
| `/eval-history … --json` | Machine-readable history report |
|
|
93
|
+
| `/eval-history --since 7d` | Grade only sessions with activity inside the window (`24h`, `30d` also parse) |
|
|
94
|
+
|
|
95
|
+
`--since` filters by each session's **latest activity**, not its creation time: a session opened long ago but active an hour ago stays in the window; a freshly created but silent one drops out. With `--since 7d` the trend compares the first and last of the in-window set, and an empty window prints a clear message instead of a crash. Without the flag, behavior is exactly as before (last N sessions).
|
|
96
|
+
|
|
97
|
+
Use `/eval-history --out report.md` to save the terminal card as Markdown, or add `--json` to save the JSON payload. Relative paths resolve against the current session workspace (the process working directory when unavailable). Parent directories must already exist; file errors are reported without creating directories. Existing output files are overwritten.
|
|
93
98
|
|
|
94
99
|
`/eval-history` finds the sessions for you via the same `sessionQuery` seam `/archive` uses (scoped to the current workspace's `cwd`), grades each, and diffs the first against the last. `/eval-diff` remains the tool when you already know the two ids that matter.
|
|
95
100
|
|
|
101
|
+
## Configuring grade bands
|
|
102
|
+
|
|
103
|
+
The thresholds are defaults, not laws. A `gradeBands` block under the plugin's config node (naturally per-profile) overrides any dimension — values are the A–D upper bounds, ascending:
|
|
104
|
+
|
|
105
|
+
```yaml
|
|
106
|
+
session-eval:
|
|
107
|
+
gradeBands:
|
|
108
|
+
reliability: [4, 8, 15, 30] # tool error-rate percent
|
|
109
|
+
reask: [0, 2, 4, 8] # corrections per 10 user turns
|
|
110
|
+
toolLoad: [0.1, 0.5, 8, 20] # tool calls per turn: [lowC, lowB, highC, highD]
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Any subset may be overridden; unlisted dimensions keep the documented defaults. Unknown keys, wrong arity, non-numeric, negative, or non-ascending values fail fast at plugin load with a readable error — sessions are never graded against bands nobody chose.
|
|
114
|
+
|
|
96
115
|
Typical use: grade the ten sessions before and after a plugin or prompt change; a persistent `Reliability` regression is an early warning that the change hurt real work, not just benchmarks.
|
|
97
116
|
|
|
98
117
|
## Dimensions & thresholds
|
package/README.zh.md
CHANGED
|
@@ -65,7 +65,7 @@ Notes:
|
|
|
65
65
|
|
|
66
66
|
- **本地开发**:`npm ci && npm run typecheck && npm test && npm run bundle`(Node 20 或 22)。
|
|
67
67
|
- **源码入口**:[`src/grade.ts`](src/grade.ts)(阈值与等级卡)、[`src/metrics.ts`](src/metrics.ts)(指标提取)、[`src/trend.ts`](src/trend.ts)(`/eval-history` 趋势逻辑)。源码地图与第一次 PR 建议见 [CONTRIBUTING.md](CONTRIBUTING.md)。
|
|
68
|
-
- **当前缺口**:[#1](https://github.com/kittimzhe/dsh-session-eval/issues/1)
|
|
68
|
+
- **当前缺口**:[#1](https://github.com/kittimzhe/dsh-session-eval/issues/1)(阈值可配置)与 [#2](https://github.com/kittimzhe/dsh-session-eval/issues/2)(`/eval-history --since`)已在 v0.3.0 落地——或浏览 [`good first issue` 标签](https://github.com/kittimzhe/dsh-session-eval/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)。
|
|
69
69
|
- **规矩**:行为变更必须带测试;文档必须 `README.md` 与 `README.zh.md` 同步改;版本发布由维护者执行。详见 [CONTRIBUTING.md](CONTRIBUTING.md)。
|
|
70
70
|
|
|
71
71
|
## 与基准式评测(dsh-eval)的区别
|
|
@@ -90,11 +90,30 @@ Notes:
|
|
|
90
90
|
| `/eval-diff … --json` | 机器可读趋势报告 |
|
|
91
91
|
| `/eval-history [N]` | 本工作区最近 N 个会话成绩单 + 首末趋势 |
|
|
92
92
|
| `/eval-history … --json` | 机器可读历史报告 |
|
|
93
|
+
| `/eval-history --since 7d` | 只评窗口内有活动的会话(也支持 `24h`、`30d`) |
|
|
94
|
+
|
|
95
|
+
`--since` 按**最新活动时间**过滤,而非创建时间:很久前打开但一小时前还活跃的会话留在窗口内;刚创建但一直沉默的会被排除。`--since 7d` 下趋势对比的是窗口内首末两个会话;窗口为空时打印明确提示而不是崩溃。不加该参数时行为与从前完全一致(最近 N 个)。
|
|
96
|
+
|
|
97
|
+
使用 `/eval-history --out report.md` 将终端卡片保存为 Markdown,添加 `--json` 则保存 JSON。相对路径基于当前会话工作目录(缺失时使用进程工作目录)。父目录必须已存在;写入失败会返回错误,不会创建目录。已存在的输出文件会被覆盖。
|
|
93
98
|
|
|
94
99
|
`/eval-history` 通过与 `/archive` 相同的 `sessionQuery` 接缝自动找到会话(按当前工作区 `cwd` 过滤),逐个评级,并把首尾两个会话做趋势对比——不用再手动挑 id。已经知道两个关键 id 时用 `/eval-diff`。
|
|
95
100
|
|
|
96
101
|
典型用法:换插件或 prompt 前后各评十个会话;`Reliability` 持续回归 = 改动伤害了真实工作的早期预警。
|
|
97
102
|
|
|
103
|
+
## 配置等级阈值
|
|
104
|
+
|
|
105
|
+
阈值是默认值而非铁律。在插件配置节点下(天然按 profile 区分)加 `gradeBands` 块即可覆盖任意维度——值为 A–D 各档上限、升序:
|
|
106
|
+
|
|
107
|
+
```yaml
|
|
108
|
+
session-eval:
|
|
109
|
+
gradeBands:
|
|
110
|
+
reliability: [4, 8, 15, 30] # 工具错误率百分比
|
|
111
|
+
reask: [0, 2, 4, 8] # 每 10 个用户轮次的纠错次数
|
|
112
|
+
toolLoad: [0.1, 0.5, 8, 20] # 每轮工具调用数:[lowC, lowB, highC, highD]
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
可只覆盖部分维度;未列出的保持文档默认值。未知键、元素个数不对、非数字、负数或非升序,都会在插件加载时以可读错误快速失败——绝不会对着没人选过的阈值评级。
|
|
116
|
+
|
|
98
117
|
## 维度与阈值(确定性,无裁判)
|
|
99
118
|
|
|
100
119
|
| 维度 | 指标 | A | B | C | D | E |
|
package/lib/index.d.ts
CHANGED
|
@@ -92,8 +92,23 @@ declare function computeMetrics(sessionId: string, events: readonly EvalEvent[])
|
|
|
92
92
|
declare function adaptEvents(raw: readonly unknown[]): EvalEvent[];
|
|
93
93
|
//#endregion
|
|
94
94
|
//#region src/grade.d.ts
|
|
95
|
-
/**
|
|
96
|
-
|
|
95
|
+
/**
|
|
96
|
+
* Grade bands per dimension, overridable from the plugin config (#1).
|
|
97
|
+
* Each array holds the A/B/C/D upper bounds in ascending order; anything
|
|
98
|
+
* above the D bound grades E. Reliability bounds are error-rate fractions,
|
|
99
|
+
* re-ask bounds are corrections per 10 user turns, tool-load bounds are
|
|
100
|
+
* calls per turn as [lowC, lowB, highC, highD].
|
|
101
|
+
*/
|
|
102
|
+
interface GradeBands {
|
|
103
|
+
readonly reliability: readonly [number, number, number, number];
|
|
104
|
+
readonly reask: readonly [number, number, number, number];
|
|
105
|
+
readonly toolLoad: readonly [number, number, number, number];
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Grade one session deterministically. `bands` may override any subset of
|
|
109
|
+
* the dimensions; unspecified ones keep the documented defaults.
|
|
110
|
+
*/
|
|
111
|
+
declare function gradeSession(m: SessionMetrics, bands?: Partial<GradeBands>): GradeCard;
|
|
97
112
|
//#endregion
|
|
98
113
|
//#region src/trend.d.ts
|
|
99
114
|
/** Compare two sessions dimension by dimension. */
|
|
@@ -128,16 +143,22 @@ interface QuerySeam {
|
|
|
128
143
|
}>;
|
|
129
144
|
}
|
|
130
145
|
/** Execute /eval against the session-query seam. */
|
|
131
|
-
declare function executeEval(ctx: Context, invocation: CommandInvocation, seam?: QuerySeam): Promise<CommandResult>;
|
|
146
|
+
declare function executeEval(ctx: Context, invocation: CommandInvocation, seam?: QuerySeam, bands?: Partial<GradeBands>): Promise<CommandResult>;
|
|
132
147
|
/** Execute /eval-diff against the session-query seam. */
|
|
133
148
|
declare function executeEvalDiff(ctx: Context, invocation: CommandInvocation, seam?: QuerySeam): Promise<CommandResult>;
|
|
134
149
|
//#endregion
|
|
135
150
|
//#region src/historyCommand.d.ts
|
|
136
|
-
declare const EVAL_HISTORY_USAGE = "Usage: /eval-history [N] [--json] (N = last N sessions, 1-20, default 5)";
|
|
151
|
+
declare const EVAL_HISTORY_USAGE = "Usage: /eval-history [N] [--json] [--out PATH] [--since 24h|7d|30d] (N = last N sessions, 1-20, default 5; --since grades only sessions whose latest activity falls inside the window)";
|
|
137
152
|
/** Args for /eval-history. */
|
|
138
153
|
interface EvalHistoryArgs {
|
|
139
154
|
readonly count: number;
|
|
140
155
|
readonly json: boolean;
|
|
156
|
+
readonly out?: string;
|
|
157
|
+
/** Time window from --since; grades only sessions whose latest activity falls inside. */
|
|
158
|
+
readonly since?: {
|
|
159
|
+
label: string;
|
|
160
|
+
ms: number;
|
|
161
|
+
};
|
|
141
162
|
}
|
|
142
163
|
/** Parse /eval-history input; returns args or a usage-error string. */
|
|
143
164
|
declare function parseEvalHistoryArgs(rawInput: string): EvalHistoryArgs | string;
|
|
@@ -152,6 +173,8 @@ interface HistoryEntry {
|
|
|
152
173
|
interface HistoryReport {
|
|
153
174
|
readonly entries: readonly HistoryEntry[];
|
|
154
175
|
readonly trend: TrendReport | null;
|
|
176
|
+
/** The --since label this report was filtered by, if any. */
|
|
177
|
+
readonly sinceLabel?: string;
|
|
155
178
|
}
|
|
156
179
|
/** The slice of sessionQuery this command consumes (structural, for testability). */
|
|
157
180
|
interface HistorySeam {
|
|
@@ -169,7 +192,7 @@ interface HistorySeam {
|
|
|
169
192
|
/** Render a history report as terminal text. */
|
|
170
193
|
declare function renderHistory(report: HistoryReport): string;
|
|
171
194
|
/** Execute /eval-history against the session-query seam. */
|
|
172
|
-
declare function executeEvalHistory(ctx: Context, invocation: CommandInvocation, seam?: HistorySeam): Promise<CommandResult>;
|
|
195
|
+
declare function executeEvalHistory(ctx: Context, invocation: CommandInvocation, seam?: HistorySeam, bands?: Partial<GradeBands>): Promise<CommandResult>;
|
|
173
196
|
//#endregion
|
|
174
197
|
//#region src/version.d.ts
|
|
175
198
|
/**
|
|
@@ -177,12 +200,18 @@ declare function executeEvalHistory(ctx: Context, invocation: CommandInvocation,
|
|
|
177
200
|
*
|
|
178
201
|
* @module dsh-session-eval/version
|
|
179
202
|
*/
|
|
180
|
-
declare const VERSION = "0.
|
|
203
|
+
declare const VERSION = "0.3.0";
|
|
181
204
|
//#endregion
|
|
182
205
|
//#region src/index.d.ts
|
|
183
206
|
declare const name = "session-eval";
|
|
184
207
|
declare const inject: string[];
|
|
185
|
-
/**
|
|
186
|
-
|
|
208
|
+
/**
|
|
209
|
+
* Plugin entry: mount the /eval, /eval-diff and /eval-history commands.
|
|
210
|
+
*
|
|
211
|
+
* `config` is the plugin's node in the host config tree (per-profile): its
|
|
212
|
+
* `gradeBands` block overrides the default grade thresholds — see config.ts.
|
|
213
|
+
* Invalid config fails fast here, at plugin load, rather than mid-grade.
|
|
214
|
+
*/
|
|
215
|
+
declare function apply(ctx: Context, config?: unknown): void;
|
|
187
216
|
//#endregion
|
|
188
217
|
export { type DimensionDelta, type DimensionGrade, EVAL_DIFF_USAGE, EVAL_HISTORY_USAGE, EVAL_USAGE, type EvalArgs, type EvalDiffArgs, type EvalEvent, type EvalEventKind, type EvalHistoryArgs, type Grade, type GradeCard, type HistoryEntry, type HistoryReport, type HistorySeam, type SessionMetrics, type TrendReport, VERSION, adaptEvents, apply, compareSessions, computeMetrics, executeEval, executeEvalDiff, executeEvalHistory, gradeSession, id8, inject, name, parseEvalArgs, parseEvalDiffArgs, parseEvalHistoryArgs, renderCard, renderHistory, renderTrend };
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
2
|
+
import { writeFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
2
4
|
//#region src/metrics.ts
|
|
3
5
|
/** Count a correction signal for every extra user message inside a consecutive-user run. */
|
|
4
6
|
function countCorrections(events) {
|
|
@@ -84,27 +86,35 @@ function readBool(source, key) {
|
|
|
84
86
|
}
|
|
85
87
|
//#endregion
|
|
86
88
|
//#region src/grade.ts
|
|
87
|
-
/**
|
|
88
|
-
const
|
|
89
|
-
[
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
]
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
89
|
+
/** The documented defaults; unchanged when no config is given. */
|
|
90
|
+
const DEFAULT_GRADE_BANDS = {
|
|
91
|
+
reliability: [
|
|
92
|
+
.05,
|
|
93
|
+
.1,
|
|
94
|
+
.2,
|
|
95
|
+
.35
|
|
96
|
+
],
|
|
97
|
+
reask: [
|
|
98
|
+
0,
|
|
99
|
+
1,
|
|
100
|
+
2,
|
|
101
|
+
4
|
|
102
|
+
],
|
|
103
|
+
toolLoad: [
|
|
104
|
+
.1,
|
|
105
|
+
.5,
|
|
106
|
+
8,
|
|
107
|
+
20
|
|
108
|
+
]
|
|
109
|
+
};
|
|
110
|
+
function gradeBy(rate, bands) {
|
|
111
|
+
const grades = [
|
|
112
|
+
"A",
|
|
113
|
+
"B",
|
|
114
|
+
"C",
|
|
115
|
+
"D"
|
|
116
|
+
];
|
|
117
|
+
for (let i = 0; i < bands.length; i++) if (rate <= bands[i]) return grades[i];
|
|
108
118
|
return "E";
|
|
109
119
|
}
|
|
110
120
|
const ORDER = [
|
|
@@ -120,8 +130,15 @@ function worst(a, b) {
|
|
|
120
130
|
function pct(rate) {
|
|
121
131
|
return `${(rate * 100).toFixed(1)}%`;
|
|
122
132
|
}
|
|
123
|
-
/**
|
|
124
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Grade one session deterministically. `bands` may override any subset of
|
|
135
|
+
* the dimensions; unspecified ones keep the documented defaults.
|
|
136
|
+
*/
|
|
137
|
+
function gradeSession(m, bands = {}) {
|
|
138
|
+
const b = {
|
|
139
|
+
...DEFAULT_GRADE_BANDS,
|
|
140
|
+
...bands
|
|
141
|
+
};
|
|
125
142
|
const dimensions = [];
|
|
126
143
|
const notes = [];
|
|
127
144
|
if (m.toolCalls === 0) {
|
|
@@ -132,7 +149,7 @@ function gradeSession(m) {
|
|
|
132
149
|
});
|
|
133
150
|
notes.push("Reliability is n/a — the session made no tool calls.");
|
|
134
151
|
} else {
|
|
135
|
-
const grade = gradeBy(m.toolErrorRate,
|
|
152
|
+
const grade = gradeBy(m.toolErrorRate, b.reliability);
|
|
136
153
|
dimensions.push({
|
|
137
154
|
dimension: "Reliability",
|
|
138
155
|
grade,
|
|
@@ -145,7 +162,7 @@ function gradeSession(m) {
|
|
|
145
162
|
detail: "no user turns"
|
|
146
163
|
});
|
|
147
164
|
else {
|
|
148
|
-
const grade = gradeBy(m.correctionRate,
|
|
165
|
+
const grade = gradeBy(m.correctionRate, b.reask);
|
|
149
166
|
dimensions.push({
|
|
150
167
|
dimension: "Re-ask",
|
|
151
168
|
grade,
|
|
@@ -158,9 +175,10 @@ function gradeSession(m) {
|
|
|
158
175
|
detail: "no tool activity"
|
|
159
176
|
});
|
|
160
177
|
else {
|
|
178
|
+
const [lowC, lowB, highC, highD] = b.toolLoad;
|
|
161
179
|
let grade = "A";
|
|
162
|
-
if (m.toolLoad <
|
|
163
|
-
if (m.toolLoad >
|
|
180
|
+
if (m.toolLoad < lowB) grade = m.toolLoad < lowC ? "C" : "B";
|
|
181
|
+
if (m.toolLoad > highC) grade = worst(grade, m.toolLoad > highD ? "D" : "C");
|
|
164
182
|
dimensions.push({
|
|
165
183
|
dimension: "Tool load",
|
|
166
184
|
grade,
|
|
@@ -227,7 +245,7 @@ function compareSessions(before, after) {
|
|
|
227
245
|
*
|
|
228
246
|
* @module dsh-session-eval/version
|
|
229
247
|
*/
|
|
230
|
-
const VERSION = "0.
|
|
248
|
+
const VERSION = "0.3.0";
|
|
231
249
|
//#endregion
|
|
232
250
|
//#region src/evalCommand.ts
|
|
233
251
|
const EVAL_USAGE = "Usage: /eval [--id <sessionId>] [--json]";
|
|
@@ -310,7 +328,7 @@ async function readEvents(ctx, seam, sessionIdRaw) {
|
|
|
310
328
|
}
|
|
311
329
|
}
|
|
312
330
|
/** Execute /eval against the session-query seam. */
|
|
313
|
-
async function executeEval(ctx, invocation, seam = ctx.sessionQuery) {
|
|
331
|
+
async function executeEval(ctx, invocation, seam = ctx.sessionQuery, bands = {}) {
|
|
314
332
|
const parsed = parseEvalArgs(invocation.rawInput);
|
|
315
333
|
if (typeof parsed === "string") return {
|
|
316
334
|
kind: "error",
|
|
@@ -323,18 +341,18 @@ async function executeEval(ctx, invocation, seam = ctx.sessionQuery) {
|
|
|
323
341
|
text: events
|
|
324
342
|
};
|
|
325
343
|
const metrics = computeMetrics(sessionIdRaw, adaptEvents(events));
|
|
326
|
-
const card = gradeSession(metrics);
|
|
344
|
+
const card = gradeSession(metrics, bands);
|
|
327
345
|
if (parsed.json === true) return {
|
|
328
346
|
kind: "success",
|
|
329
347
|
text: JSON.stringify({
|
|
330
|
-
generator: "dsh-session-eval v0.
|
|
348
|
+
generator: "dsh-session-eval v0.3.0",
|
|
331
349
|
metrics,
|
|
332
350
|
card
|
|
333
351
|
}, null, 2)
|
|
334
352
|
};
|
|
335
353
|
return {
|
|
336
354
|
kind: "success",
|
|
337
|
-
text: renderCard(card, "dsh-session-eval v0.
|
|
355
|
+
text: renderCard(card, "dsh-session-eval v0.3.0")
|
|
338
356
|
};
|
|
339
357
|
}
|
|
340
358
|
/** Execute /eval-diff against the session-query seam. */
|
|
@@ -358,25 +376,58 @@ async function executeEvalDiff(ctx, invocation, seam = ctx.sessionQuery) {
|
|
|
358
376
|
if (parsed.json === true) return {
|
|
359
377
|
kind: "success",
|
|
360
378
|
text: JSON.stringify({
|
|
361
|
-
generator: "dsh-session-eval v0.
|
|
379
|
+
generator: "dsh-session-eval v0.3.0",
|
|
362
380
|
report
|
|
363
381
|
}, null, 2)
|
|
364
382
|
};
|
|
365
383
|
return {
|
|
366
384
|
kind: "success",
|
|
367
|
-
text: renderTrend(report, "dsh-session-eval v0.
|
|
385
|
+
text: renderTrend(report, "dsh-session-eval v0.3.0")
|
|
368
386
|
};
|
|
369
387
|
}
|
|
370
388
|
//#endregion
|
|
371
389
|
//#region src/historyCommand.ts
|
|
372
|
-
|
|
390
|
+
/**
|
|
391
|
+
* `/eval-history` — grade the last N sessions in this workspace and show the trend.
|
|
392
|
+
*
|
|
393
|
+
* Completes the regression-tracking story: `/eval-diff` compares two sessions
|
|
394
|
+
* you already know about; this command finds them for you (via
|
|
395
|
+
* `ctx.sessionQuery.listSessions()`, same seam as `/archive` in
|
|
396
|
+
* dsh-session-export) and answers "is the trend improving?" out of the box.
|
|
397
|
+
*
|
|
398
|
+
* @module dsh-session-eval/historyCommand
|
|
399
|
+
*/
|
|
400
|
+
const EVAL_HISTORY_USAGE = "Usage: /eval-history [N] [--json] [--out PATH] [--since 24h|7d|30d] (N = last N sessions, 1-20, default 5; --since grades only sessions whose latest activity falls inside the window)";
|
|
373
401
|
/** Parse /eval-history input; returns args or a usage-error string. */
|
|
374
402
|
function parseEvalHistoryArgs(rawInput) {
|
|
375
403
|
const tokens = rawInput.trim().split(/\s+/).filter((t) => t.length > 0);
|
|
376
404
|
let count = 5;
|
|
377
405
|
let json = false;
|
|
378
406
|
let sawCount = false;
|
|
379
|
-
|
|
407
|
+
let out;
|
|
408
|
+
let since;
|
|
409
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
410
|
+
const token = tokens[i];
|
|
411
|
+
if (token === "--since") {
|
|
412
|
+
if (since !== void 0) return `--since may be given only once.\n${EVAL_HISTORY_USAGE}`;
|
|
413
|
+
const value = tokens[++i];
|
|
414
|
+
const match = /^(\d+)(h|d)$/.exec(value ?? "");
|
|
415
|
+
if (!match) return `--since needs a window like 24h or 7d.\n${EVAL_HISTORY_USAGE}`;
|
|
416
|
+
const amount = Number(match[1]);
|
|
417
|
+
if (amount < 1 || amount > 365) return `--since window is out of range.\n${EVAL_HISTORY_USAGE}`;
|
|
418
|
+
since = {
|
|
419
|
+
label: value,
|
|
420
|
+
ms: amount * (match[2] === "h" ? 36e5 : 864e5)
|
|
421
|
+
};
|
|
422
|
+
continue;
|
|
423
|
+
}
|
|
424
|
+
if (token === "--out") {
|
|
425
|
+
if (out !== void 0) return `--out may be given only once.\n${EVAL_HISTORY_USAGE}`;
|
|
426
|
+
const path = tokens[++i];
|
|
427
|
+
if (!path || path.startsWith("--")) return `--out requires a path.\n${EVAL_HISTORY_USAGE}`;
|
|
428
|
+
out = path;
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
380
431
|
if (token === "--json") {
|
|
381
432
|
if (json) return `--json may be given only once.\n${EVAL_HISTORY_USAGE}`;
|
|
382
433
|
json = true;
|
|
@@ -392,7 +443,9 @@ function parseEvalHistoryArgs(rawInput) {
|
|
|
392
443
|
}
|
|
393
444
|
return {
|
|
394
445
|
count,
|
|
395
|
-
json
|
|
446
|
+
json,
|
|
447
|
+
...out !== void 0 ? { out } : {},
|
|
448
|
+
...since !== void 0 ? { since } : {}
|
|
396
449
|
};
|
|
397
450
|
}
|
|
398
451
|
function toMillis(createdAt) {
|
|
@@ -410,7 +463,10 @@ function stamp(ms) {
|
|
|
410
463
|
/** Render a history report as terminal text. */
|
|
411
464
|
function renderHistory(report) {
|
|
412
465
|
const lines = [];
|
|
413
|
-
if (report.entries.length === 0)
|
|
466
|
+
if (report.entries.length === 0) {
|
|
467
|
+
if (report.sinceLabel !== void 0) return `No sessions with activity in the last ${report.sinceLabel} in this workspace; nothing to grade.`;
|
|
468
|
+
return "No sessions found in this workspace; nothing to grade.";
|
|
469
|
+
}
|
|
414
470
|
lines.push(`Session history — last ${report.entries.length} in this workspace`);
|
|
415
471
|
for (const entry of report.entries) {
|
|
416
472
|
const m = entry.metrics;
|
|
@@ -421,7 +477,7 @@ function renderHistory(report) {
|
|
|
421
477
|
return lines.join("\n");
|
|
422
478
|
}
|
|
423
479
|
/** Execute /eval-history against the session-query seam. */
|
|
424
|
-
async function executeEvalHistory(ctx, invocation, seam = ctx.sessionQuery) {
|
|
480
|
+
async function executeEvalHistory(ctx, invocation, seam = ctx.sessionQuery, bands = {}) {
|
|
425
481
|
const parsed = parseEvalHistoryArgs(invocation.rawInput);
|
|
426
482
|
if (typeof parsed === "string") return {
|
|
427
483
|
kind: "error",
|
|
@@ -437,18 +493,37 @@ async function executeEvalHistory(ctx, invocation, seam = ctx.sessionQuery) {
|
|
|
437
493
|
text: `Could not list sessions: ${error instanceof Error ? error.message : String(error)}`
|
|
438
494
|
};
|
|
439
495
|
}
|
|
440
|
-
const
|
|
496
|
+
const headers = records.map((record) => record.header).filter((header) => header != null).filter((header) => ownCwd === void 0 || header.cwd === ownCwd).map((header) => ({
|
|
441
497
|
id: String(header.id),
|
|
442
498
|
createdAt: toMillis(header.createdAt)
|
|
443
|
-
})).sort((a, b) => (a.createdAt ?? 0) - (b.createdAt ?? 0))
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
499
|
+
})).sort((a, b) => (a.createdAt ?? 0) - (b.createdAt ?? 0));
|
|
500
|
+
const activityCache = /* @__PURE__ */ new Map();
|
|
501
|
+
let recent = headers.slice(-parsed.count);
|
|
502
|
+
if (parsed.since !== void 0) {
|
|
503
|
+
const cutoff = Date.now() - parsed.since.ms;
|
|
504
|
+
const chosen = [];
|
|
505
|
+
for (let i = headers.length - 1; i >= 0 && chosen.length < parsed.count; i--) {
|
|
506
|
+
const header = headers[i];
|
|
507
|
+
let log;
|
|
508
|
+
try {
|
|
509
|
+
log = await seam.readSession(SessionId(header.id));
|
|
510
|
+
} catch (error) {
|
|
511
|
+
return {
|
|
512
|
+
kind: "error",
|
|
513
|
+
text: `Could not read session ${id8(header.id)}: ${error instanceof Error ? error.message : String(error)}`
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
activityCache.set(header.id, log.events);
|
|
517
|
+
if (adaptEvents(log.events).reduce((max, event) => Math.max(max, event.time), Number.NEGATIVE_INFINITY) >= cutoff) chosen.unshift(header);
|
|
518
|
+
}
|
|
519
|
+
recent = chosen;
|
|
520
|
+
}
|
|
448
521
|
const entries = [];
|
|
449
522
|
for (const header of recent) {
|
|
523
|
+
const cached = activityCache.get(header.id);
|
|
450
524
|
let events;
|
|
451
|
-
|
|
525
|
+
if (cached !== void 0) events = cached;
|
|
526
|
+
else try {
|
|
452
527
|
events = (await seam.readSession(SessionId(header.id))).events;
|
|
453
528
|
} catch (error) {
|
|
454
529
|
return {
|
|
@@ -461,36 +536,119 @@ async function executeEvalHistory(ctx, invocation, seam = ctx.sessionQuery) {
|
|
|
461
536
|
sessionId: header.id,
|
|
462
537
|
createdAt: header.createdAt,
|
|
463
538
|
metrics,
|
|
464
|
-
card: gradeSession(metrics)
|
|
539
|
+
card: gradeSession(metrics, bands)
|
|
465
540
|
});
|
|
466
541
|
}
|
|
467
542
|
const report = {
|
|
468
543
|
entries,
|
|
469
|
-
trend: entries.length >= 2 ? compareSessions(entries[0].metrics, entries[entries.length - 1].metrics) : null
|
|
544
|
+
trend: entries.length >= 2 ? compareSessions(entries[0].metrics, entries[entries.length - 1].metrics) : null,
|
|
545
|
+
...parsed.since !== void 0 ? { sinceLabel: parsed.since.label } : {}
|
|
470
546
|
};
|
|
471
|
-
|
|
547
|
+
const text = parsed.json ? JSON.stringify({
|
|
548
|
+
generator: `dsh-session-eval v${VERSION}`,
|
|
549
|
+
report
|
|
550
|
+
}, null, 2) : renderHistory(report);
|
|
551
|
+
if (parsed.out !== void 0) {
|
|
552
|
+
const path = resolve(typeof ownCwd === "string" ? ownCwd : process.cwd(), parsed.out);
|
|
553
|
+
try {
|
|
554
|
+
const output = parsed.json ? text : `\`\`\`text\n${text}\n\`\`\``;
|
|
555
|
+
await writeFile(path, `${output}\n`, "utf8");
|
|
556
|
+
} catch (error) {
|
|
557
|
+
return {
|
|
558
|
+
kind: "error",
|
|
559
|
+
text: `Could not write history report to ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return {
|
|
472
564
|
kind: "success",
|
|
473
|
-
text
|
|
474
|
-
generator: `dsh-session-eval v${VERSION}`,
|
|
475
|
-
report
|
|
476
|
-
}, null, 2)
|
|
565
|
+
text
|
|
477
566
|
};
|
|
567
|
+
}
|
|
568
|
+
//#endregion
|
|
569
|
+
//#region src/config.ts
|
|
570
|
+
/**
|
|
571
|
+
* Plugin config parsing for dsh-session-eval.
|
|
572
|
+
*
|
|
573
|
+
* The config block lives under the plugin's node in the host config tree, so
|
|
574
|
+
* it is naturally per-profile: each profile can grade against its own bands.
|
|
575
|
+
*
|
|
576
|
+
* Shape (#1):
|
|
577
|
+
*
|
|
578
|
+
* ```yaml
|
|
579
|
+
* session-eval:
|
|
580
|
+
* gradeBands:
|
|
581
|
+
* reliability: [4, 8, 15, 30] # tool error-rate percent, A–D upper bounds
|
|
582
|
+
* reask: [0, 2, 4, 8] # corrections per 10 user turns
|
|
583
|
+
* toolLoad: [0.1, 0.5, 8, 20] # tool calls per turn: [lowC, lowB, highC, highD]
|
|
584
|
+
* ```
|
|
585
|
+
*
|
|
586
|
+
* Unknown keys, wrong arity, non-numeric, negative or non-ascending values
|
|
587
|
+
* fail fast with a readable error — a silent fallback would grade sessions
|
|
588
|
+
* against bands nobody chose.
|
|
589
|
+
*
|
|
590
|
+
* @module dsh-session-eval/config
|
|
591
|
+
*/
|
|
592
|
+
const DIMENSIONS = [
|
|
593
|
+
"reliability",
|
|
594
|
+
"reask",
|
|
595
|
+
"toolLoad"
|
|
596
|
+
];
|
|
597
|
+
const SHAPE = "4 ascending numbers: reliability = tool error-rate percent A–D upper bounds, reask = corrections per 10 user turns, toolLoad = [lowC, lowB, highC, highD] tool calls per turn";
|
|
598
|
+
function parseDimension(dimension, raw) {
|
|
599
|
+
const where = `gradeBands.${dimension}`;
|
|
600
|
+
if (raw.length !== 4) throw new Error(`${where} needs exactly ${SHAPE}`);
|
|
601
|
+
const values = raw.map((entry) => {
|
|
602
|
+
if (typeof entry !== "number" || !Number.isFinite(entry)) throw new Error(`${where} entries must each be a finite number (${SHAPE})`);
|
|
603
|
+
return entry;
|
|
604
|
+
});
|
|
605
|
+
for (const value of values) if (value < 0) throw new Error(`${where} entries must not be negative (${SHAPE})`);
|
|
606
|
+
for (let i = 1; i < values.length; i++) if (values[i] <= values[i - 1]) throw new Error(`${where} entries must be strictly ascending (${SHAPE})`);
|
|
607
|
+
return values;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Parse the plugin config into grade bands. Returns the defaults unchanged
|
|
611
|
+
* when nothing is configured; throws a readable Error on any invalid input.
|
|
612
|
+
*/
|
|
613
|
+
function parseGradeBands(config) {
|
|
614
|
+
if (config === void 0 || config === null) return DEFAULT_GRADE_BANDS;
|
|
615
|
+
if (typeof config !== "object") throw new Error("session-eval config must be a mapping");
|
|
616
|
+
const root = config;
|
|
617
|
+
for (const key of Object.keys(root)) if (key !== "gradeBands") throw new Error(`Unknown session-eval config key "${key}" — only "gradeBands" is read`);
|
|
618
|
+
const raw = root.gradeBands;
|
|
619
|
+
if (raw === void 0) return DEFAULT_GRADE_BANDS;
|
|
620
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) throw new Error("session-eval gradeBands must be a mapping");
|
|
621
|
+
const overrides = {};
|
|
622
|
+
for (const key of Object.keys(raw)) if (!DIMENSIONS.includes(key)) throw new Error(`Unknown gradeBands dimension "${key}" — expected one of ${DIMENSIONS.join(", ")}`);
|
|
623
|
+
for (const dimension of DIMENSIONS) {
|
|
624
|
+
const entry = raw[dimension];
|
|
625
|
+
if (entry !== void 0) overrides[dimension] = parseDimension(dimension, entry);
|
|
626
|
+
}
|
|
627
|
+
const quad = (dimension) => overrides[dimension];
|
|
478
628
|
return {
|
|
479
|
-
|
|
480
|
-
|
|
629
|
+
reliability: overrides.reliability !== void 0 ? overrides.reliability.map((v) => v / 100) : DEFAULT_GRADE_BANDS.reliability,
|
|
630
|
+
reask: overrides.reask !== void 0 ? quad("reask") : DEFAULT_GRADE_BANDS.reask,
|
|
631
|
+
toolLoad: overrides.toolLoad !== void 0 ? quad("toolLoad") : DEFAULT_GRADE_BANDS.toolLoad
|
|
481
632
|
};
|
|
482
633
|
}
|
|
483
634
|
//#endregion
|
|
484
635
|
//#region src/index.ts
|
|
485
636
|
const name = "session-eval";
|
|
486
637
|
const inject = ["commands", "sessionQuery"];
|
|
487
|
-
/**
|
|
488
|
-
|
|
638
|
+
/**
|
|
639
|
+
* Plugin entry: mount the /eval, /eval-diff and /eval-history commands.
|
|
640
|
+
*
|
|
641
|
+
* `config` is the plugin's node in the host config tree (per-profile): its
|
|
642
|
+
* `gradeBands` block overrides the default grade thresholds — see config.ts.
|
|
643
|
+
* Invalid config fails fast here, at plugin load, rather than mid-grade.
|
|
644
|
+
*/
|
|
645
|
+
function apply(ctx, config) {
|
|
646
|
+
const bands = parseGradeBands(config);
|
|
489
647
|
ctx.effect(function* () {
|
|
490
648
|
yield ctx.commands.register({
|
|
491
649
|
name: "eval",
|
|
492
650
|
description: "Print a deterministic grade card for this session (or another via --id): reliability, re-ask, tool load",
|
|
493
|
-
handler: (invocation) => executeEval(ctx, invocation)
|
|
651
|
+
handler: (invocation) => executeEval(ctx, invocation, void 0, bands)
|
|
494
652
|
});
|
|
495
653
|
yield ctx.commands.register({
|
|
496
654
|
name: "eval-diff",
|
|
@@ -500,7 +658,7 @@ function apply(ctx) {
|
|
|
500
658
|
yield ctx.commands.register({
|
|
501
659
|
name: "eval-history",
|
|
502
660
|
description: "Grade the last N sessions in this workspace (default 5) and show whether the trend is improving",
|
|
503
|
-
handler: (invocation) => executeEvalHistory(ctx, invocation)
|
|
661
|
+
handler: (invocation) => executeEvalHistory(ctx, invocation, void 0, bands)
|
|
504
662
|
});
|
|
505
663
|
}, "session-eval lifecycle");
|
|
506
664
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-session-eval",
|
|
3
3
|
"description": "Retrospective agent-session evaluation for DeepSeek Harness: deterministic grade cards from persisted session logs, cross-session regression diffs, era comparison — no benchmark runs, no LLM judges.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|