dsh-session-eval 0.2.4 → 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 +7 -0
- package/README.md +18 -1
- package/README.zh.md +18 -1
- package/lib/index.d.ts +36 -8
- package/lib/index.js +178 -47
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
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
|
+
|
|
1
8
|
## 0.2.4 — 2026-10-11
|
|
2
9
|
|
|
3
10
|
Feature release: eval-history report files.
|
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,11 +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).
|
|
93
96
|
|
|
94
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.
|
|
95
98
|
|
|
96
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.
|
|
97
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
|
+
|
|
98
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.
|
|
99
116
|
|
|
100
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,6 +90,9 @@ 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 个)。
|
|
93
96
|
|
|
94
97
|
使用 `/eval-history --out report.md` 将终端卡片保存为 Markdown,添加 `--json` 则保存 JSON。相对路径基于当前会话工作目录(缺失时使用进程工作目录)。父目录必须已存在;写入失败会返回错误,不会创建目录。已存在的输出文件会被覆盖。
|
|
95
98
|
|
|
@@ -97,6 +100,20 @@ Notes:
|
|
|
97
100
|
|
|
98
101
|
典型用法:换插件或 prompt 前后各评十个会话;`Reliability` 持续回归 = 改动伤害了真实工作的早期预警。
|
|
99
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
|
+
|
|
100
117
|
## 维度与阈值(确定性,无裁判)
|
|
101
118
|
|
|
102
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,17 +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] [--out PATH] (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;
|
|
141
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
|
+
};
|
|
142
162
|
}
|
|
143
163
|
/** Parse /eval-history input; returns args or a usage-error string. */
|
|
144
164
|
declare function parseEvalHistoryArgs(rawInput: string): EvalHistoryArgs | string;
|
|
@@ -153,6 +173,8 @@ interface HistoryEntry {
|
|
|
153
173
|
interface HistoryReport {
|
|
154
174
|
readonly entries: readonly HistoryEntry[];
|
|
155
175
|
readonly trend: TrendReport | null;
|
|
176
|
+
/** The --since label this report was filtered by, if any. */
|
|
177
|
+
readonly sinceLabel?: string;
|
|
156
178
|
}
|
|
157
179
|
/** The slice of sessionQuery this command consumes (structural, for testability). */
|
|
158
180
|
interface HistorySeam {
|
|
@@ -170,7 +192,7 @@ interface HistorySeam {
|
|
|
170
192
|
/** Render a history report as terminal text. */
|
|
171
193
|
declare function renderHistory(report: HistoryReport): string;
|
|
172
194
|
/** Execute /eval-history against the session-query seam. */
|
|
173
|
-
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>;
|
|
174
196
|
//#endregion
|
|
175
197
|
//#region src/version.d.ts
|
|
176
198
|
/**
|
|
@@ -178,12 +200,18 @@ declare function executeEvalHistory(ctx: Context, invocation: CommandInvocation,
|
|
|
178
200
|
*
|
|
179
201
|
* @module dsh-session-eval/version
|
|
180
202
|
*/
|
|
181
|
-
declare const VERSION = "0.
|
|
203
|
+
declare const VERSION = "0.3.0";
|
|
182
204
|
//#endregion
|
|
183
205
|
//#region src/index.d.ts
|
|
184
206
|
declare const name = "session-eval";
|
|
185
207
|
declare const inject: string[];
|
|
186
|
-
/**
|
|
187
|
-
|
|
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;
|
|
188
216
|
//#endregion
|
|
189
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
|
@@ -86,27 +86,35 @@ function readBool(source, key) {
|
|
|
86
86
|
}
|
|
87
87
|
//#endregion
|
|
88
88
|
//#region src/grade.ts
|
|
89
|
-
/**
|
|
90
|
-
const
|
|
91
|
-
[
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
]
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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];
|
|
110
118
|
return "E";
|
|
111
119
|
}
|
|
112
120
|
const ORDER = [
|
|
@@ -122,8 +130,15 @@ function worst(a, b) {
|
|
|
122
130
|
function pct(rate) {
|
|
123
131
|
return `${(rate * 100).toFixed(1)}%`;
|
|
124
132
|
}
|
|
125
|
-
/**
|
|
126
|
-
|
|
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
|
+
};
|
|
127
142
|
const dimensions = [];
|
|
128
143
|
const notes = [];
|
|
129
144
|
if (m.toolCalls === 0) {
|
|
@@ -134,7 +149,7 @@ function gradeSession(m) {
|
|
|
134
149
|
});
|
|
135
150
|
notes.push("Reliability is n/a — the session made no tool calls.");
|
|
136
151
|
} else {
|
|
137
|
-
const grade = gradeBy(m.toolErrorRate,
|
|
152
|
+
const grade = gradeBy(m.toolErrorRate, b.reliability);
|
|
138
153
|
dimensions.push({
|
|
139
154
|
dimension: "Reliability",
|
|
140
155
|
grade,
|
|
@@ -147,7 +162,7 @@ function gradeSession(m) {
|
|
|
147
162
|
detail: "no user turns"
|
|
148
163
|
});
|
|
149
164
|
else {
|
|
150
|
-
const grade = gradeBy(m.correctionRate,
|
|
165
|
+
const grade = gradeBy(m.correctionRate, b.reask);
|
|
151
166
|
dimensions.push({
|
|
152
167
|
dimension: "Re-ask",
|
|
153
168
|
grade,
|
|
@@ -160,9 +175,10 @@ function gradeSession(m) {
|
|
|
160
175
|
detail: "no tool activity"
|
|
161
176
|
});
|
|
162
177
|
else {
|
|
178
|
+
const [lowC, lowB, highC, highD] = b.toolLoad;
|
|
163
179
|
let grade = "A";
|
|
164
|
-
if (m.toolLoad <
|
|
165
|
-
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");
|
|
166
182
|
dimensions.push({
|
|
167
183
|
dimension: "Tool load",
|
|
168
184
|
grade,
|
|
@@ -229,7 +245,7 @@ function compareSessions(before, after) {
|
|
|
229
245
|
*
|
|
230
246
|
* @module dsh-session-eval/version
|
|
231
247
|
*/
|
|
232
|
-
const VERSION = "0.
|
|
248
|
+
const VERSION = "0.3.0";
|
|
233
249
|
//#endregion
|
|
234
250
|
//#region src/evalCommand.ts
|
|
235
251
|
const EVAL_USAGE = "Usage: /eval [--id <sessionId>] [--json]";
|
|
@@ -312,7 +328,7 @@ async function readEvents(ctx, seam, sessionIdRaw) {
|
|
|
312
328
|
}
|
|
313
329
|
}
|
|
314
330
|
/** Execute /eval against the session-query seam. */
|
|
315
|
-
async function executeEval(ctx, invocation, seam = ctx.sessionQuery) {
|
|
331
|
+
async function executeEval(ctx, invocation, seam = ctx.sessionQuery, bands = {}) {
|
|
316
332
|
const parsed = parseEvalArgs(invocation.rawInput);
|
|
317
333
|
if (typeof parsed === "string") return {
|
|
318
334
|
kind: "error",
|
|
@@ -325,18 +341,18 @@ async function executeEval(ctx, invocation, seam = ctx.sessionQuery) {
|
|
|
325
341
|
text: events
|
|
326
342
|
};
|
|
327
343
|
const metrics = computeMetrics(sessionIdRaw, adaptEvents(events));
|
|
328
|
-
const card = gradeSession(metrics);
|
|
344
|
+
const card = gradeSession(metrics, bands);
|
|
329
345
|
if (parsed.json === true) return {
|
|
330
346
|
kind: "success",
|
|
331
347
|
text: JSON.stringify({
|
|
332
|
-
generator: "dsh-session-eval v0.
|
|
348
|
+
generator: "dsh-session-eval v0.3.0",
|
|
333
349
|
metrics,
|
|
334
350
|
card
|
|
335
351
|
}, null, 2)
|
|
336
352
|
};
|
|
337
353
|
return {
|
|
338
354
|
kind: "success",
|
|
339
|
-
text: renderCard(card, "dsh-session-eval v0.
|
|
355
|
+
text: renderCard(card, "dsh-session-eval v0.3.0")
|
|
340
356
|
};
|
|
341
357
|
}
|
|
342
358
|
/** Execute /eval-diff against the session-query seam. */
|
|
@@ -360,13 +376,13 @@ async function executeEvalDiff(ctx, invocation, seam = ctx.sessionQuery) {
|
|
|
360
376
|
if (parsed.json === true) return {
|
|
361
377
|
kind: "success",
|
|
362
378
|
text: JSON.stringify({
|
|
363
|
-
generator: "dsh-session-eval v0.
|
|
379
|
+
generator: "dsh-session-eval v0.3.0",
|
|
364
380
|
report
|
|
365
381
|
}, null, 2)
|
|
366
382
|
};
|
|
367
383
|
return {
|
|
368
384
|
kind: "success",
|
|
369
|
-
text: renderTrend(report, "dsh-session-eval v0.
|
|
385
|
+
text: renderTrend(report, "dsh-session-eval v0.3.0")
|
|
370
386
|
};
|
|
371
387
|
}
|
|
372
388
|
//#endregion
|
|
@@ -381,7 +397,7 @@ async function executeEvalDiff(ctx, invocation, seam = ctx.sessionQuery) {
|
|
|
381
397
|
*
|
|
382
398
|
* @module dsh-session-eval/historyCommand
|
|
383
399
|
*/
|
|
384
|
-
const EVAL_HISTORY_USAGE = "Usage: /eval-history [N] [--json] [--out PATH] (N = last N sessions, 1-20, default 5)";
|
|
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)";
|
|
385
401
|
/** Parse /eval-history input; returns args or a usage-error string. */
|
|
386
402
|
function parseEvalHistoryArgs(rawInput) {
|
|
387
403
|
const tokens = rawInput.trim().split(/\s+/).filter((t) => t.length > 0);
|
|
@@ -389,8 +405,22 @@ function parseEvalHistoryArgs(rawInput) {
|
|
|
389
405
|
let json = false;
|
|
390
406
|
let sawCount = false;
|
|
391
407
|
let out;
|
|
408
|
+
let since;
|
|
392
409
|
for (let i = 0; i < tokens.length; i++) {
|
|
393
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
|
+
}
|
|
394
424
|
if (token === "--out") {
|
|
395
425
|
if (out !== void 0) return `--out may be given only once.\n${EVAL_HISTORY_USAGE}`;
|
|
396
426
|
const path = tokens[++i];
|
|
@@ -414,7 +444,8 @@ function parseEvalHistoryArgs(rawInput) {
|
|
|
414
444
|
return {
|
|
415
445
|
count,
|
|
416
446
|
json,
|
|
417
|
-
...out !== void 0 ? { out } : {}
|
|
447
|
+
...out !== void 0 ? { out } : {},
|
|
448
|
+
...since !== void 0 ? { since } : {}
|
|
418
449
|
};
|
|
419
450
|
}
|
|
420
451
|
function toMillis(createdAt) {
|
|
@@ -432,7 +463,10 @@ function stamp(ms) {
|
|
|
432
463
|
/** Render a history report as terminal text. */
|
|
433
464
|
function renderHistory(report) {
|
|
434
465
|
const lines = [];
|
|
435
|
-
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
|
+
}
|
|
436
470
|
lines.push(`Session history — last ${report.entries.length} in this workspace`);
|
|
437
471
|
for (const entry of report.entries) {
|
|
438
472
|
const m = entry.metrics;
|
|
@@ -443,7 +477,7 @@ function renderHistory(report) {
|
|
|
443
477
|
return lines.join("\n");
|
|
444
478
|
}
|
|
445
479
|
/** Execute /eval-history against the session-query seam. */
|
|
446
|
-
async function executeEvalHistory(ctx, invocation, seam = ctx.sessionQuery) {
|
|
480
|
+
async function executeEvalHistory(ctx, invocation, seam = ctx.sessionQuery, bands = {}) {
|
|
447
481
|
const parsed = parseEvalHistoryArgs(invocation.rawInput);
|
|
448
482
|
if (typeof parsed === "string") return {
|
|
449
483
|
kind: "error",
|
|
@@ -459,14 +493,37 @@ async function executeEvalHistory(ctx, invocation, seam = ctx.sessionQuery) {
|
|
|
459
493
|
text: `Could not list sessions: ${error instanceof Error ? error.message : String(error)}`
|
|
460
494
|
};
|
|
461
495
|
}
|
|
462
|
-
const
|
|
496
|
+
const headers = records.map((record) => record.header).filter((header) => header != null).filter((header) => ownCwd === void 0 || header.cwd === ownCwd).map((header) => ({
|
|
463
497
|
id: String(header.id),
|
|
464
498
|
createdAt: toMillis(header.createdAt)
|
|
465
|
-
})).sort((a, b) => (a.createdAt ?? 0) - (b.createdAt ?? 0))
|
|
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
|
+
}
|
|
466
521
|
const entries = [];
|
|
467
522
|
for (const header of recent) {
|
|
523
|
+
const cached = activityCache.get(header.id);
|
|
468
524
|
let events;
|
|
469
|
-
|
|
525
|
+
if (cached !== void 0) events = cached;
|
|
526
|
+
else try {
|
|
470
527
|
events = (await seam.readSession(SessionId(header.id))).events;
|
|
471
528
|
} catch (error) {
|
|
472
529
|
return {
|
|
@@ -479,12 +536,13 @@ async function executeEvalHistory(ctx, invocation, seam = ctx.sessionQuery) {
|
|
|
479
536
|
sessionId: header.id,
|
|
480
537
|
createdAt: header.createdAt,
|
|
481
538
|
metrics,
|
|
482
|
-
card: gradeSession(metrics)
|
|
539
|
+
card: gradeSession(metrics, bands)
|
|
483
540
|
});
|
|
484
541
|
}
|
|
485
542
|
const report = {
|
|
486
543
|
entries,
|
|
487
|
-
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 } : {}
|
|
488
546
|
};
|
|
489
547
|
const text = parsed.json ? JSON.stringify({
|
|
490
548
|
generator: `dsh-session-eval v${VERSION}`,
|
|
@@ -508,16 +566,89 @@ async function executeEvalHistory(ctx, invocation, seam = ctx.sessionQuery) {
|
|
|
508
566
|
};
|
|
509
567
|
}
|
|
510
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];
|
|
628
|
+
return {
|
|
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
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
//#endregion
|
|
511
635
|
//#region src/index.ts
|
|
512
636
|
const name = "session-eval";
|
|
513
637
|
const inject = ["commands", "sessionQuery"];
|
|
514
|
-
/**
|
|
515
|
-
|
|
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);
|
|
516
647
|
ctx.effect(function* () {
|
|
517
648
|
yield ctx.commands.register({
|
|
518
649
|
name: "eval",
|
|
519
650
|
description: "Print a deterministic grade card for this session (or another via --id): reliability, re-ask, tool load",
|
|
520
|
-
handler: (invocation) => executeEval(ctx, invocation)
|
|
651
|
+
handler: (invocation) => executeEval(ctx, invocation, void 0, bands)
|
|
521
652
|
});
|
|
522
653
|
yield ctx.commands.register({
|
|
523
654
|
name: "eval-diff",
|
|
@@ -527,7 +658,7 @@ function apply(ctx) {
|
|
|
527
658
|
yield ctx.commands.register({
|
|
528
659
|
name: "eval-history",
|
|
529
660
|
description: "Grade the last N sessions in this workspace (default 5) and show whether the trend is improving",
|
|
530
|
-
handler: (invocation) => executeEvalHistory(ctx, invocation)
|
|
661
|
+
handler: (invocation) => executeEvalHistory(ctx, invocation, void 0, bands)
|
|
531
662
|
});
|
|
532
663
|
}, "session-eval lifecycle");
|
|
533
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
|
},
|