dsh-session-eval 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 kittimzhe
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # dsh-session-eval
2
+
3
+ English | [中文](README.zh.md)
4
+
5
+ [![npm](https://img.shields.io/npm/v/dsh-session-eval)](https://www.npmjs.com/package/dsh-session-eval)
6
+ [![npm downloads](https://img.shields.io/npm/dw/dsh-session-eval)](https://www.npmjs.com/package/dsh-session-eval)
7
+ [![tests](https://github.com/kittimzhe/dsh-session-eval/actions/workflows/test.yml/badge.svg)](https://github.com/kittimzhe/dsh-session-eval/actions/workflows/test.yml)
8
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
9
+
10
+ **Retrospective evaluation for DeepSeek Harness agent sessions** — deterministic grade cards computed from sessions that already happened, plus cross-session regression diffs that answer *"did my agent get better or worse after that plugin / prompt / model change?"*
11
+
12
+ No benchmark suites to author. No LLM judges. No new runs. Every grade is a fixed, documented threshold over persisted session logs — fully reproducible.
13
+
14
+ ## The measurement layer of the session toolchain
15
+
16
+ | Plugin | Layer | Answers |
17
+ |---|---|---|
18
+ | [`dsh-session-export`](https://www.npmjs.com/package/dsh-session-export) | Evidence | "What exactly happened in this session?" |
19
+ | [`dsh-session-recall`](https://www.npmjs.com/package/dsh-session-recall) | Memory | "What did I do before, and where is it?" |
20
+ | **`dsh-session-eval`** | **Measurement** | **"Was that session good? Is the trend improving?"** |
21
+
22
+ All three read through the same trusted `ctx.sessionQuery` seam, so any persistence backend (JSONL or SQLite) works without touching raw artifacts.
23
+
24
+ ## How it differs from benchmark-style evaluation
25
+
26
+ | | `dsh-eval` (benchmark) | `dsh-session-eval` (this) |
27
+ |---|---|---|
28
+ | Evaluates | Runs you launch ahead of time | Sessions that already happened |
29
+ | Setup | Author benchmark YAML + cases | None — read existing logs |
30
+ | Judge | Metric folding over trials | Fixed thresholds, deterministic |
31
+ | Best at | Pre-deploy benchmarks | Regression tracking over real work |
32
+
33
+ Both are useful; they don't compete. Benchmarks test what you *predicted*; retrospective evaluation watches what *actually happened*.
34
+
35
+ ## Quick Start
36
+
37
+ ```bash
38
+ npm install -g dsh-session-eval
39
+ ```
40
+
41
+ Then in DeepSeek Harness:
42
+
43
+ ```text
44
+ /eval
45
+ ```
46
+
47
+ ```text
48
+ Session eval — a1b2c3d4… (dsh-session-eval v0.1.0)
49
+ Overall: A
50
+ Reliability A 2/87 tool results errored (2.3%)
51
+ Re-ask A 1 re-ask signal(s) over 23 turns (0.4/10 turns)
52
+ Tool load B 3.8 tool calls per turn
53
+ Notes:
54
+ • Wall time 41.2 min over 23 turns.
55
+ ```
56
+
57
+ Grade another session, or emit machine-readable JSON:
58
+
59
+ ```text
60
+ /eval --id <sessionId> --json
61
+ ```
62
+
63
+ Compare two sessions — the regression workflow:
64
+
65
+ ```text
66
+ /eval-diff <beforeSessionId> <afterSessionId>
67
+ ```
68
+
69
+ ```text
70
+ Session diff — a1b2c3d4… → e5f6a7b8… (dsh-session-eval v0.1.0)
71
+ Overall: regressed — 0 improved, 2 regressed, 1 flat.
72
+ Reliability 2.1% → 11.8% regressed
73
+ Re-ask 0.5 → 3.2 regressed
74
+ Tool load 3.8 → 4.1 flat
75
+ ```
76
+
77
+ 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.
78
+
79
+ ## Dimensions & thresholds
80
+
81
+ All grades derive from three deterministic metrics. `n/a` when the session lacks the relevant activity.
82
+
83
+ | Dimension | Metric | A | B | C | D | E |
84
+ |---|---|---|---|---|---|---|
85
+ | Reliability | tool error rate | ≤5% | ≤10% | ≤20% | ≤35% | >35% |
86
+ | Re-ask | corrections per 10 turns¹ | 0 | <1 | <2 | <4 | ≥4 |
87
+ | Tool load | tool calls per turn | 0.5–8 | 0.1–0.5 or just above 8 | <0.1 or 8–20 | >20 | — |
88
+
89
+ ¹ A *correction signal* is each extra user message inside a consecutive-user run — the human re-asking before any assistant reply, a behavioral proxy for "the agent didn't get it right the first time."
90
+
91
+ `Overall` is the **worst** graded dimension — conservative by design. Thresholds are constants in [`src/grade.ts`](src/grade.ts); they will be configurable in a future release.
92
+
93
+ Diff verdicts include a noise band (±2pp error rate, ±0.5 re-ask, ±1 tool load) so trivial jitter doesn't cry regression.
94
+
95
+ ## Determinism guarantee
96
+
97
+ Same session logs in → same grade card out, every time, forever. There is no model in the loop, no sampling, no clock dependence in the metrics (wall time is reported, never graded). This is what makes `/eval-diff` trustworthy as a regression signal.
98
+
99
+ ## Commands
100
+
101
+ | Command | Effect |
102
+ |---|---|
103
+ | `/eval` | Grade card for the current session |
104
+ | `/eval --id <sessionId>` | Grade a specific session |
105
+ | `/eval --json` | Machine-readable metrics + card |
106
+ | `/eval-diff <beforeId> <afterId>` | Regression comparison, noise-tolerant |
107
+ | `/eval-diff … --json` | Machine-readable trend report |
108
+
109
+ ## Development
110
+
111
+ ```bash
112
+ npm install
113
+ npm run typecheck # tsc --noEmit
114
+ npm test # vitest run — 24 tests
115
+ npm run bundle # tsdown → lib/
116
+ ```
117
+
118
+ ## License
119
+
120
+ [MIT](LICENSE)
package/README.zh.md ADDED
@@ -0,0 +1,67 @@
1
+ # dsh-session-eval
2
+
3
+ [![npm](https://img.shields.io/npm/v/dsh-session-eval)](https://www.npmjs.com/package/dsh-session-eval)
4
+ [![tests](https://github.com/kittimzhe/dsh-session-eval/actions/workflows/test.yml/badge.svg)](https://github.com/kittimzhe/dsh-session-eval/actions/workflows/test.yml)
5
+ [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
+
7
+ **DeepSeek Harness 会话的回溯式评测**:对已经发生的会话做确定性等级评定,跨会话回归对比,回答"换了插件 / prompt / 模型之后,我的 agent 是变好了还是变差了?"
8
+
9
+ 不需要编写基准测试集,没有 LLM 裁判,不需要跑新任务。所有等级 = 持久化会话日志上的固定公开阈值——完全可复现。
10
+
11
+ English | [中文](README.zh.md)
12
+
13
+ ## 会话工具链的评测层
14
+
15
+ | 插件 | 层 | 回答的问题 |
16
+ |---|---|---|
17
+ | `dsh-session-export` | 证据层 | "这个会话到底发生了什么?" |
18
+ | `dsh-session-recall` | 记忆层 | "我以前做过什么,在哪?" |
19
+ | **`dsh-session-eval`** | **评测层** | **"刚才的会话好不好?趋势在变好吗?"** |
20
+
21
+ 三者都通过同一个可信的 `ctx.sessionQuery` 缝读取,JSONL / SQLite 等任何持久化后端均可使用。
22
+
23
+ ## 与基准式评测(dsh-eval)的区别
24
+
25
+ - `dsh-eval`:**前瞻**——预先写好基准 YAML,跑无头评测,评的是"你预测到的场景"
26
+ - 本插件:**回溯**——直接读存量日志,评的是"真实发生的工作",零准备、零额外 token
27
+
28
+ 两者互补而非竞争。
29
+
30
+ ## 快速开始
31
+
32
+ ```bash
33
+ npm install -g dsh-session-eval
34
+ ```
35
+
36
+ ```text
37
+ /eval # 当前会话等级卡
38
+ /eval --id <会话ID> # 指定会话
39
+ /eval-diff <前> <后> # 回归对比(换插件/prompt 前后各评一次)
40
+ ```
41
+
42
+ ## 维度与阈值(确定性,无裁判)
43
+
44
+ | 维度 | 指标 | A | B | C | D | E |
45
+ |---|---|---|---|---|---|---|
46
+ | Reliability | 工具错误率 | ≤5% | ≤10% | ≤20% | ≤35% | >35% |
47
+ | Re-ask | 每 10 轮纠正信号¹ | 0 | <1 | <2 | <4 | ≥4 |
48
+ | Tool load | 每轮工具调用 | 0.5–8 | 0.1–0.5 | <0.1 或 8–20 | >20 | — |
49
+
50
+ ¹ 纠正信号 = 连续用户消息中多出的那条(agent 还没回复用户就重问)——"第一次没做对"的行为学代理指标。
51
+
52
+ 总评 = 最差维度(保守设计)。Diff 判定带噪声带(错误率 ±2pp 等),微小抖动不会误报回归。
53
+
54
+ ## 确定性保证
55
+
56
+ 同样的日志进 → 同样的等级出。没有模型在环、没有采样、指标不含时钟依赖(wall time 只报告不评级)。这是 `/eval-diff` 能作为回归信号被信任的原因。
57
+
58
+ ## 开发
59
+
60
+ ```bash
61
+ npm install && npm run typecheck && npm test # 24 个测试
62
+ npm run bundle
63
+ ```
64
+
65
+ ## 许可
66
+
67
+ [MIT](LICENSE)
package/lib/index.d.ts ADDED
@@ -0,0 +1,141 @@
1
+ import { SessionId } from "@deepseek-ai/dsh-session";
2
+ import { Context } from "@deepseek-ai/cordis";
3
+ import { CommandInvocation, CommandResult } from "@deepseek-ai/dsh-commands";
4
+ //#region src/types.d.ts
5
+ /**
6
+ * Core types for retrospective session evaluation.
7
+ *
8
+ * The plugin projects raw session events into a small event vocabulary once
9
+ * (see {@link adaptEvents}), then every metric, grade, and trend computation
10
+ * is pure over plain data — no LLM judges, no network, no nondeterminism.
11
+ *
12
+ * @module dsh-session-eval/types
13
+ */
14
+ /** Event kinds the evaluation layer reasons about. */
15
+ type EvalEventKind = 'user' | 'assistant' | 'tool_call' | 'tool_result';
16
+ /** One projected session event. */
17
+ interface EvalEvent {
18
+ /** Raw-log sequence number of the source event. */
19
+ readonly seq: number;
20
+ /** Event time in epoch milliseconds. */
21
+ readonly time: number;
22
+ /** Projected kind. */
23
+ readonly kind: EvalEventKind;
24
+ /** Tool name for `tool_call` / `tool_result` events. */
25
+ readonly tool?: string;
26
+ /** Whether a `tool_result` reported an error. */
27
+ readonly isError?: boolean;
28
+ }
29
+ /** Deterministic per-session metrics. */
30
+ interface SessionMetrics {
31
+ readonly sessionId: string;
32
+ readonly events: number;
33
+ readonly userTurns: number;
34
+ readonly assistantMessages: number;
35
+ readonly toolCalls: number;
36
+ readonly toolErrors: number;
37
+ /** toolErrors / max(1, toolCalls). 0 when no tool calls. */
38
+ readonly toolErrorRate: number;
39
+ /** Extra user messages inside consecutive-user runs (re-ask signals). */
40
+ readonly corrections: number;
41
+ /** corrections per 10 user turns. */
42
+ readonly correctionRate: number;
43
+ /** toolCalls / max(1, userTurns). */
44
+ readonly toolLoad: number;
45
+ /** wall-clock span in ms (last.time - first.time); 0 for empty sessions. */
46
+ readonly wallMs: number;
47
+ readonly firstTime: number | null;
48
+ readonly lastTime: number | null;
49
+ }
50
+ /** Letter grade for one dimension. */
51
+ type Grade = 'A' | 'B' | 'C' | 'D' | 'E';
52
+ /** One graded dimension. */
53
+ interface DimensionGrade {
54
+ readonly dimension: 'Reliability' | 'Re-ask' | 'Tool load';
55
+ readonly grade: Grade | 'n/a';
56
+ readonly detail: string;
57
+ }
58
+ /** Deterministic grade card for one session. */
59
+ interface GradeCard {
60
+ readonly sessionId: string;
61
+ readonly dimensions: readonly DimensionGrade[];
62
+ /** Worst non-n/a dimension grade; 'n/a' when all dimensions are n/a. */
63
+ readonly overall: Grade | 'n/a';
64
+ readonly notes: readonly string[];
65
+ }
66
+ /** Verdict for one dimension between two sessions. */
67
+ interface DimensionDelta {
68
+ readonly dimension: DimensionGrade['dimension'];
69
+ readonly before: string;
70
+ readonly after: string;
71
+ readonly verdict: 'improved' | 'regressed' | 'flat';
72
+ }
73
+ /** Cross-session regression report. */
74
+ interface TrendReport {
75
+ readonly before: SessionMetrics;
76
+ readonly after: SessionMetrics;
77
+ readonly deltas: readonly DimensionDelta[];
78
+ readonly overall: 'improved' | 'regressed' | 'flat';
79
+ readonly summary: string;
80
+ }
81
+ //#endregion
82
+ //#region src/metrics.d.ts
83
+ /** Compute the deterministic metric set for one session. */
84
+ declare function computeMetrics(sessionId: string, events: readonly EvalEvent[]): SessionMetrics;
85
+ /**
86
+ * Project raw session-log events into the evaluation vocabulary.
87
+ *
88
+ * Defensive by design: the adapter matches the `user/message`,
89
+ * `assistant/message`, `tool/call`, `tool/result` event-type vocabulary
90
+ * (plus role-based fallbacks) and never throws — unknown shapes are skipped.
91
+ */
92
+ declare function adaptEvents(raw: readonly unknown[]): EvalEvent[];
93
+ //#endregion
94
+ //#region src/grade.d.ts
95
+ /** Grade one session deterministically. */
96
+ declare function gradeSession(m: SessionMetrics): GradeCard;
97
+ //#endregion
98
+ //#region src/trend.d.ts
99
+ /** Compare two sessions dimension by dimension. */
100
+ declare function compareSessions(before: SessionMetrics, after: SessionMetrics): TrendReport;
101
+ //#endregion
102
+ //#region src/evalCommand.d.ts
103
+ declare const EVAL_USAGE = "Usage: /eval [--id <sessionId>] [--json]";
104
+ declare const EVAL_DIFF_USAGE = "Usage: /eval-diff <beforeSessionId> <afterSessionId> [--json]";
105
+ declare function id8(sessionId: string): string;
106
+ /** Args for /eval. */
107
+ interface EvalArgs {
108
+ readonly sessionId?: string;
109
+ readonly json?: boolean;
110
+ }
111
+ /** Args for /eval-diff. */
112
+ interface EvalDiffArgs {
113
+ readonly beforeId: string;
114
+ readonly afterId: string;
115
+ readonly json?: boolean;
116
+ }
117
+ /** Parse /eval input; returns args or a usage-error string. */
118
+ declare function parseEvalArgs(rawInput: string): EvalArgs | string;
119
+ /** Parse /eval-diff input; returns args or a usage-error string. */
120
+ declare function parseEvalDiffArgs(rawInput: string): EvalDiffArgs | string;
121
+ /** Render a grade card as terminal text. */
122
+ declare function renderCard(card: GradeCard, generator: string): string;
123
+ /** Render a trend report as terminal text. */
124
+ declare function renderTrend(report: TrendReport, generator: string): string;
125
+ interface QuerySeam {
126
+ readSession(id: ReturnType<typeof SessionId>): Promise<{
127
+ events: readonly unknown[];
128
+ }>;
129
+ }
130
+ /** Execute /eval against the session-query seam. */
131
+ declare function executeEval(ctx: Context, invocation: CommandInvocation, seam?: QuerySeam): Promise<CommandResult>;
132
+ /** Execute /eval-diff against the session-query seam. */
133
+ declare function executeEvalDiff(ctx: Context, invocation: CommandInvocation, seam?: QuerySeam): Promise<CommandResult>;
134
+ //#endregion
135
+ //#region src/index.d.ts
136
+ declare const name = "session-eval";
137
+ declare const inject: string[];
138
+ /** Plugin entry: mount the /eval and /eval-diff commands. */
139
+ declare function apply(ctx: Context): void;
140
+ //#endregion
141
+ export { type DimensionDelta, type DimensionGrade, EVAL_DIFF_USAGE, EVAL_USAGE, type EvalArgs, type EvalDiffArgs, type EvalEvent, type EvalEventKind, type Grade, type GradeCard, type SessionMetrics, type TrendReport, adaptEvents, apply, compareSessions, computeMetrics, executeEval, executeEvalDiff, gradeSession, id8, inject, name, parseEvalArgs, parseEvalDiffArgs, renderCard, renderTrend };
package/lib/index.js ADDED
@@ -0,0 +1,382 @@
1
+ import { SessionId } from "@deepseek-ai/dsh-session";
2
+ //#region src/metrics.ts
3
+ /** Count a correction signal for every extra user message inside a consecutive-user run. */
4
+ function countCorrections(events) {
5
+ let corrections = 0;
6
+ let runLength = 0;
7
+ for (const event of events) if (event.kind === "user") {
8
+ runLength += 1;
9
+ if (runLength >= 2) corrections += 1;
10
+ } else runLength = 0;
11
+ return corrections;
12
+ }
13
+ /** Compute the deterministic metric set for one session. */
14
+ function computeMetrics(sessionId, events) {
15
+ const userTurns = events.filter((e) => e.kind === "user").length;
16
+ const assistantMessages = events.filter((e) => e.kind === "assistant").length;
17
+ const toolEvents = events.filter((e) => e.kind === "tool_call");
18
+ const toolResults = events.filter((e) => e.kind === "tool_result");
19
+ const toolCalls = Math.max(toolEvents.length, toolResults.length);
20
+ const toolErrors = toolResults.filter((e) => e.isError === true).length;
21
+ const corrections = countCorrections(events);
22
+ const times = events.map((e) => e.time).filter((t) => Number.isFinite(t));
23
+ const firstTime = times.length > 0 ? Math.min(...times) : null;
24
+ const lastTime = times.length > 0 ? Math.max(...times) : null;
25
+ return {
26
+ sessionId,
27
+ events: events.length,
28
+ userTurns,
29
+ assistantMessages,
30
+ toolCalls,
31
+ toolErrors,
32
+ toolErrorRate: toolCalls > 0 ? toolErrors / toolCalls : 0,
33
+ corrections,
34
+ correctionRate: userTurns > 0 ? corrections / userTurns * 10 : 0,
35
+ toolLoad: userTurns > 0 ? toolCalls / userTurns : 0,
36
+ wallMs: firstTime !== null && lastTime !== null ? lastTime - firstTime : 0,
37
+ firstTime,
38
+ lastTime
39
+ };
40
+ }
41
+ /**
42
+ * Project raw session-log events into the evaluation vocabulary.
43
+ *
44
+ * Defensive by design: the adapter matches the `user/message`,
45
+ * `assistant/message`, `tool/call`, `tool/result` event-type vocabulary
46
+ * (plus role-based fallbacks) and never throws — unknown shapes are skipped.
47
+ */
48
+ function adaptEvents(raw) {
49
+ const out = [];
50
+ for (let i = 0; i < raw.length; i += 1) {
51
+ const event = raw[i];
52
+ if (event == null || typeof event !== "object") continue;
53
+ const seq = typeof event.seq === "number" ? event.seq : i;
54
+ const time = typeof event.time === "number" ? event.time : 0;
55
+ const message = event.message ?? event.payload;
56
+ const kind = classify(String(event.type ?? message?.type ?? ""), message);
57
+ if (kind === null) continue;
58
+ const isError = readBool(message, "isError") ?? readBool(message, "is_error") ?? readBool(event, "isError");
59
+ const tool = typeof message?.tool === "string" ? message.tool : typeof message?.name === "string" ? message.name : void 0;
60
+ out.push({
61
+ seq,
62
+ time,
63
+ kind,
64
+ tool: kind === "tool_call" || kind === "tool_result" ? tool : void 0,
65
+ isError: kind === "tool_result" ? isError === true : void 0
66
+ });
67
+ }
68
+ return out.sort((a, b) => a.seq - b.seq);
69
+ }
70
+ function classify(type, message) {
71
+ const t = type.toLowerCase();
72
+ if (t.includes("user/message") || t === "user") return "user";
73
+ if (t.includes("assistant/message") || t === "assistant") return "assistant";
74
+ if (t.includes("tool/call") || t.includes("tool_call")) return "tool_call";
75
+ if (t.includes("tool/result") || t.includes("tool_result")) return "tool_result";
76
+ const role = message?.role;
77
+ if (role === "user") return "user";
78
+ if (role === "assistant") return "assistant";
79
+ return null;
80
+ }
81
+ function readBool(source, key) {
82
+ const value = source?.[key];
83
+ return typeof value === "boolean" ? value : void 0;
84
+ }
85
+ //#endregion
86
+ //#region src/grade.ts
87
+ /** Reliability thresholds on tool error rate (fraction). */
88
+ const RELIABILITY = [
89
+ ["A", .05],
90
+ ["B", .1],
91
+ ["C", .2],
92
+ ["D", .35]
93
+ ];
94
+ /** Re-ask thresholds: corrections per 10 user turns. */
95
+ const REASK = [
96
+ ["A", 0],
97
+ ["B", 1],
98
+ ["C", 2],
99
+ ["D", 4]
100
+ ];
101
+ /** Tool-load thresholds: tool calls per user turn (both churn extremes graded). */
102
+ const TOOL_LOAD_LOW_B = .5;
103
+ const TOOL_LOAD_LOW_C = .1;
104
+ const TOOL_LOAD_HIGH_C = 8;
105
+ const TOOL_LOAD_HIGH_D = 20;
106
+ function gradeBy(rate, table) {
107
+ for (const [grade, max] of table) if (rate <= max) return grade;
108
+ return "E";
109
+ }
110
+ const ORDER = [
111
+ "A",
112
+ "B",
113
+ "C",
114
+ "D",
115
+ "E"
116
+ ];
117
+ function worst(a, b) {
118
+ return ORDER.indexOf(a) >= ORDER.indexOf(b) ? a : b;
119
+ }
120
+ function pct(rate) {
121
+ return `${(rate * 100).toFixed(1)}%`;
122
+ }
123
+ /** Grade one session deterministically. */
124
+ function gradeSession(m) {
125
+ const dimensions = [];
126
+ const notes = [];
127
+ if (m.toolCalls === 0) {
128
+ dimensions.push({
129
+ dimension: "Reliability",
130
+ grade: "n/a",
131
+ detail: "no tool calls"
132
+ });
133
+ notes.push("Reliability is n/a — the session made no tool calls.");
134
+ } else {
135
+ const grade = gradeBy(m.toolErrorRate, RELIABILITY);
136
+ dimensions.push({
137
+ dimension: "Reliability",
138
+ grade,
139
+ detail: `${m.toolErrors}/${m.toolCalls} tool results errored (${pct(m.toolErrorRate)})`
140
+ });
141
+ }
142
+ if (m.userTurns === 0) dimensions.push({
143
+ dimension: "Re-ask",
144
+ grade: "n/a",
145
+ detail: "no user turns"
146
+ });
147
+ else {
148
+ const grade = gradeBy(m.correctionRate, REASK);
149
+ dimensions.push({
150
+ dimension: "Re-ask",
151
+ grade,
152
+ detail: `${m.corrections} re-ask signal(s) over ${m.userTurns} turns (${m.correctionRate.toFixed(1)}/10 turns)`
153
+ });
154
+ }
155
+ if (m.userTurns === 0 || m.toolCalls === 0) dimensions.push({
156
+ dimension: "Tool load",
157
+ grade: "n/a",
158
+ detail: "no tool activity"
159
+ });
160
+ else {
161
+ let grade = "A";
162
+ if (m.toolLoad < TOOL_LOAD_LOW_B) grade = m.toolLoad < TOOL_LOAD_LOW_C ? "C" : "B";
163
+ if (m.toolLoad > TOOL_LOAD_HIGH_C) grade = worst(grade, m.toolLoad > TOOL_LOAD_HIGH_D ? "D" : "C");
164
+ dimensions.push({
165
+ dimension: "Tool load",
166
+ grade,
167
+ detail: `${m.toolLoad.toFixed(1)} tool calls per turn`
168
+ });
169
+ }
170
+ if (m.events === 0) notes.push("Session is empty — all dimensions are provisional.");
171
+ if (m.wallMs > 0 && m.userTurns > 0) notes.push(`Wall time ${(m.wallMs / 6e4).toFixed(1)} min over ${m.userTurns} turns.`);
172
+ const graded = dimensions.filter((d) => d.grade !== "n/a");
173
+ const overall = graded.length === 0 ? "n/a" : graded.reduce((acc, d) => worst(acc, d.grade), "A");
174
+ return {
175
+ sessionId: m.sessionId,
176
+ dimensions,
177
+ overall,
178
+ notes
179
+ };
180
+ }
181
+ //#endregion
182
+ //#region src/trend.ts
183
+ /** Minimum absolute change treated as signal (not noise). */
184
+ const ERROR_RATE_EPSILON = .02;
185
+ const REASK_EPSILON = .5;
186
+ const TOOL_LOAD_EPSILON = 1;
187
+ function verdict(before, after, eps, lowerIsBetter) {
188
+ const delta = after - before;
189
+ if (Math.abs(delta) <= eps) return "flat";
190
+ return (lowerIsBetter ? delta < 0 : delta > 0) ? "improved" : "regressed";
191
+ }
192
+ /** Compare two sessions dimension by dimension. */
193
+ function compareSessions(before, after) {
194
+ const deltas = [];
195
+ if (before.toolCalls > 0 || after.toolCalls > 0) deltas.push({
196
+ dimension: "Reliability",
197
+ before: `${(before.toolErrorRate * 100).toFixed(1)}%`,
198
+ after: `${(after.toolErrorRate * 100).toFixed(1)}%`,
199
+ verdict: verdict(before.toolErrorRate, after.toolErrorRate, ERROR_RATE_EPSILON, true)
200
+ });
201
+ if (before.userTurns > 0 || after.userTurns > 0) deltas.push({
202
+ dimension: "Re-ask",
203
+ before: before.correctionRate.toFixed(1),
204
+ after: after.correctionRate.toFixed(1),
205
+ verdict: verdict(before.correctionRate, after.correctionRate, REASK_EPSILON, true)
206
+ });
207
+ if (before.toolCalls > 0 || after.toolCalls > 0) deltas.push({
208
+ dimension: "Tool load",
209
+ before: before.toolLoad.toFixed(1),
210
+ after: after.toolLoad.toFixed(1),
211
+ verdict: verdict(before.toolLoad, after.toolLoad, TOOL_LOAD_EPSILON, false)
212
+ });
213
+ const improved = deltas.filter((d) => d.verdict === "improved").length;
214
+ const regressed = deltas.filter((d) => d.verdict === "regressed").length;
215
+ return {
216
+ before,
217
+ after,
218
+ deltas,
219
+ overall: regressed > improved ? "regressed" : improved > regressed ? "improved" : "flat",
220
+ summary: deltas.length === 0 ? "No comparable dimensions — both sessions lack tool or turn activity." : `${improved} improved, ${regressed} regressed, ${deltas.length - improved - regressed} flat.`
221
+ };
222
+ }
223
+ //#endregion
224
+ //#region src/evalCommand.ts
225
+ const EVAL_USAGE = "Usage: /eval [--id <sessionId>] [--json]";
226
+ const EVAL_DIFF_USAGE = "Usage: /eval-diff <beforeSessionId> <afterSessionId> [--json]";
227
+ function id8(sessionId) {
228
+ return sessionId.length > 8 ? `${sessionId.slice(0, 8)}…` : sessionId;
229
+ }
230
+ /** Parse /eval input; returns args or a usage-error string. */
231
+ function parseEvalArgs(rawInput) {
232
+ const trimmed = rawInput.trim();
233
+ if (trimmed.length === 0) return {};
234
+ const tokens = trimmed.split(/\s+/);
235
+ const args = {};
236
+ let i = 0;
237
+ while (i < tokens.length) {
238
+ const token = tokens[i];
239
+ if (token === void 0) break;
240
+ if (token === "--id") {
241
+ const value = tokens[i + 1];
242
+ if (value === void 0 || value.startsWith("--")) return `--id requires a session id value.\n${EVAL_USAGE}`;
243
+ if (args.sessionId !== void 0) return `--id may be given only once.\n${EVAL_USAGE}`;
244
+ args.sessionId = value;
245
+ i += 2;
246
+ continue;
247
+ }
248
+ if (token === "--json") {
249
+ if (args.json === true) return `--json may be given only once.\n${EVAL_USAGE}`;
250
+ args.json = true;
251
+ i += 1;
252
+ continue;
253
+ }
254
+ return `Unknown argument: ${token}\n${EVAL_USAGE}`;
255
+ }
256
+ return args;
257
+ }
258
+ /** Parse /eval-diff input; returns args or a usage-error string. */
259
+ function parseEvalDiffArgs(rawInput) {
260
+ const tokens = rawInput.trim().split(/\s+/).filter((t) => t.length > 0);
261
+ let json = false;
262
+ const positional = [];
263
+ for (const token of tokens) {
264
+ if (token === "--json") {
265
+ if (json) return `--json may be given only once.\n${EVAL_DIFF_USAGE}`;
266
+ json = true;
267
+ continue;
268
+ }
269
+ if (token.startsWith("--")) return `Unknown argument: ${token}\n${EVAL_DIFF_USAGE}`;
270
+ positional.push(token);
271
+ }
272
+ if (positional.length !== 2) return `Expected exactly two session ids (before, after).\n${EVAL_DIFF_USAGE}`;
273
+ const [beforeId, afterId] = positional;
274
+ if (beforeId === afterId) return `The two session ids must differ.\n${EVAL_DIFF_USAGE}`;
275
+ return {
276
+ beforeId,
277
+ afterId,
278
+ json
279
+ };
280
+ }
281
+ /** Render a grade card as terminal text. */
282
+ function renderCard(card, generator) {
283
+ const lines = [`Session eval — ${id8(card.sessionId)} (${generator})`, `Overall: ${card.overall}`];
284
+ for (const dimension of card.dimensions) lines.push(` ${dimension.dimension.padEnd(13)} ${String(dimension.grade).padEnd(3)} ${dimension.detail}`);
285
+ if (card.notes.length > 0) {
286
+ lines.push("Notes:");
287
+ for (const note of card.notes) lines.push(` • ${note}`);
288
+ }
289
+ return lines.join("\n");
290
+ }
291
+ /** Render a trend report as terminal text. */
292
+ function renderTrend(report, generator) {
293
+ const lines = [`Session diff — ${id8(report.before.sessionId)} → ${id8(report.after.sessionId)} (${generator})`, `Overall: ${report.overall} — ${report.summary}`];
294
+ for (const delta of report.deltas) lines.push(` ${delta.dimension.padEnd(13)} ${delta.before} → ${delta.after} ${delta.verdict}`);
295
+ return lines.join("\n");
296
+ }
297
+ async function readEvents(ctx, seam, sessionIdRaw) {
298
+ try {
299
+ return (await seam.readSession(SessionId(sessionIdRaw))).events;
300
+ } catch (error) {
301
+ return `Could not read session ${id8(sessionIdRaw)}: ${error instanceof Error ? error.message : String(error)}`;
302
+ }
303
+ }
304
+ /** Execute /eval against the session-query seam. */
305
+ async function executeEval(ctx, invocation, seam = ctx.sessionQuery) {
306
+ const parsed = parseEvalArgs(invocation.rawInput);
307
+ if (typeof parsed === "string") return {
308
+ kind: "error",
309
+ text: parsed
310
+ };
311
+ const sessionIdRaw = parsed.sessionId ?? String(invocation.agent.session.id);
312
+ const events = await readEvents(ctx, seam, sessionIdRaw);
313
+ if (typeof events === "string") return {
314
+ kind: "error",
315
+ text: events
316
+ };
317
+ const metrics = computeMetrics(sessionIdRaw, adaptEvents(events));
318
+ const card = gradeSession(metrics);
319
+ if (parsed.json === true) return {
320
+ kind: "success",
321
+ text: JSON.stringify({
322
+ generator: "dsh-session-eval v0.1.0",
323
+ metrics,
324
+ card
325
+ }, null, 2)
326
+ };
327
+ return {
328
+ kind: "success",
329
+ text: renderCard(card, "dsh-session-eval v0.1.0")
330
+ };
331
+ }
332
+ /** Execute /eval-diff against the session-query seam. */
333
+ async function executeEvalDiff(ctx, invocation, seam = ctx.sessionQuery) {
334
+ const parsed = parseEvalDiffArgs(invocation.rawInput);
335
+ if (typeof parsed === "string") return {
336
+ kind: "error",
337
+ text: parsed
338
+ };
339
+ const beforeEvents = await readEvents(ctx, seam, parsed.beforeId);
340
+ if (typeof beforeEvents === "string") return {
341
+ kind: "error",
342
+ text: beforeEvents
343
+ };
344
+ const afterEvents = await readEvents(ctx, seam, parsed.afterId);
345
+ if (typeof afterEvents === "string") return {
346
+ kind: "error",
347
+ text: afterEvents
348
+ };
349
+ const report = compareSessions(computeMetrics(parsed.beforeId, adaptEvents(beforeEvents)), computeMetrics(parsed.afterId, adaptEvents(afterEvents)));
350
+ if (parsed.json === true) return {
351
+ kind: "success",
352
+ text: JSON.stringify({
353
+ generator: "dsh-session-eval v0.1.0",
354
+ report
355
+ }, null, 2)
356
+ };
357
+ return {
358
+ kind: "success",
359
+ text: renderTrend(report, "dsh-session-eval v0.1.0")
360
+ };
361
+ }
362
+ //#endregion
363
+ //#region src/index.ts
364
+ const name = "session-eval";
365
+ const inject = ["commands", "sessionQuery"];
366
+ /** Plugin entry: mount the /eval and /eval-diff commands. */
367
+ function apply(ctx) {
368
+ ctx.effect(function* () {
369
+ yield ctx.commands.register({
370
+ name: "eval",
371
+ description: "Print a deterministic grade card for this session (or another via --id): reliability, re-ask, tool load",
372
+ handler: (invocation) => executeEval(ctx, invocation)
373
+ });
374
+ yield ctx.commands.register({
375
+ name: "eval-diff",
376
+ description: "Compare two sessions (before, after) and report which dimensions improved or regressed",
377
+ handler: (invocation) => executeEvalDiff(ctx, invocation)
378
+ });
379
+ }, "session-eval lifecycle");
380
+ }
381
+ //#endregion
382
+ export { EVAL_DIFF_USAGE, EVAL_USAGE, adaptEvents, apply, compareSessions, computeMetrics, executeEval, executeEvalDiff, gradeSession, id8, inject, name, parseEvalArgs, parseEvalDiffArgs, renderCard, renderTrend };
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "dsh-session-eval",
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.1.0",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "type": "module",
9
+ "main": "lib/index.js",
10
+ "types": "lib/index.d.ts",
11
+ "exports": {
12
+ ".": {
13
+ "types": "./lib/index.d.ts",
14
+ "default": "./lib/index.js"
15
+ },
16
+ "./package.json": "./package.json"
17
+ },
18
+ "files": [
19
+ "lib",
20
+ "README.md",
21
+ "README.zh.md",
22
+ "CHANGELOG.md",
23
+ "LICENSE",
24
+ "cordis.patch.yml"
25
+ ],
26
+ "dsh": {
27
+ "bundle": {
28
+ "patch": "./cordis.patch.yml"
29
+ }
30
+ },
31
+ "license": "MIT",
32
+ "keywords": [
33
+ "deepseek-harness",
34
+ "dsh",
35
+ "dsh-plugin",
36
+ "agent-evaluation",
37
+ "session-metrics",
38
+ "regression"
39
+ ],
40
+ "peerDependencies": {
41
+ "@deepseek-ai/cordis": "^4.0.1",
42
+ "@deepseek-ai/dsh-commands": "^0.1.1-rc.2",
43
+ "@deepseek-ai/dsh-llm": "^0.1.1-rc.2",
44
+ "@deepseek-ai/dsh-session": "^0.1.1-rc.2",
45
+ "@deepseek-ai/dsh-session-query": "^0.1.1-rc.2",
46
+ "@deepseek-ai/dsh-tools": "^0.1.1-rc.2"
47
+ },
48
+ "devDependencies": {
49
+ "@types/node": "^26.2.0",
50
+ "tsdown": "^0.22.14",
51
+ "typescript": "^7.0.2",
52
+ "vitest": "^4.1.11",
53
+ "@deepseek-ai/cordis": "^4.0.1",
54
+ "@deepseek-ai/dsh-commands": "^0.1.1-rc.2",
55
+ "@deepseek-ai/dsh-session": "^0.1.1-rc.2"
56
+ },
57
+ "scripts": {
58
+ "bundle": "tsdown",
59
+ "typecheck": "tsc --noEmit",
60
+ "test": "vitest run"
61
+ },
62
+ "peerDependenciesMeta": {
63
+ "@deepseek-ai/dsh-tools": {
64
+ "optional": true
65
+ }
66
+ },
67
+ "repository": {
68
+ "type": "git",
69
+ "url": "git+https://github.com/kittimzhe/dsh-session-eval.git"
70
+ },
71
+ "homepage": "https://github.com/kittimzhe/dsh-session-eval#readme",
72
+ "bugs": {
73
+ "url": "https://github.com/kittimzhe/dsh-session-eval/issues"
74
+ }
75
+ }