@deepseek-ai/dsh-llm-replay 0.1.2-alpha.5 → 0.1.3-alpha.2
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/README.i18n.yaml +2 -2
- package/README.md +9 -6
- package/README.zh.md +9 -6
- package/lib/index.js +328 -69
- package/lib/types/index.d.ts +29 -16
- package/package.json +11 -10
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/test-support/llm-replay/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: e8a257c2ee9f95f1e7d20197e6db67b2b4c7b45c
|
|
6
|
+
README.zh.md: 5d085959982bcaa090a893089c0295426141bed2
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ English | [中文](README.zh.md)
|
|
|
9
9
|
|
|
10
10
|
## Summary
|
|
11
11
|
|
|
12
|
-
`dsh-llm-replay` makes snapshot tests run without an API key: it installs a replay LLM adapter that serves model streams reconstructed from a recorded session JSONL fixture, so a test boots the real agent against a fixed transcript. The fixture is a projection of the persisted session log — `assistant/
|
|
12
|
+
`dsh-llm-replay` makes snapshot tests run without an API key: it installs a replay LLM adapter that serves model streams reconstructed from a recorded session JSONL fixture, so a test boots the real agent against a fixed transcript. The fixture is a projection of the persisted session log — each `assistant/message` or `assistant/attempt` embeds one model-call stream, and an explicitly marked local compaction call replays as one canonical stream. A `replay.override.json` sidecar covers what a settlement cannot reconstruct: a throw before any chunk, a cancel/hang, or an injected retry. Live sessions bind to recorded scripts by first-call order, so parent-and-subagent scenarios each get their own script. It is the model source behind the ACP and headless snapshot suites and the Web browser e2e lane.
|
|
13
13
|
|
|
14
14
|
## Table of Contents
|
|
15
15
|
|
|
@@ -55,7 +55,7 @@ With `providers` configured, the plugin registers a replay-only adapter whose ca
|
|
|
55
55
|
|
|
56
56
|
| Field | Default | Meaning |
|
|
57
57
|
|---|---|---|
|
|
58
|
-
| `file` | `$DSH_SNAPSHOT_FILE` | Path to the primary
|
|
58
|
+
| `file` | `$DSH_SNAPSHOT_FILE` | Path to the selected primary fixture: `session.jsonl` for v0 or `session.vN.jsonl` for a positive generation; required (config or env) |
|
|
59
59
|
| `overrideFile` | `$DSH_SNAPSHOT_OVERRIDE` | Optional `ReplayOverrideDoc` sidecar for the primary session |
|
|
60
60
|
| `childFiles` | `$DSH_SNAPSHOT_CHILD_FILES` | Recorded subagent child-session logs for a nested scenario |
|
|
61
61
|
| `providers` | — | Optional replay-only provider and model catalog; a model may declare `contextWindow`, text/image modalities, and positive `imageRequestTokens` when image-capable; invalid values fail at load and routes never perform provider I/O |
|
|
@@ -65,17 +65,17 @@ The generated [configuration catalog](../../../docs/config-catalog.md#deepseek-a
|
|
|
65
65
|
|
|
66
66
|
### How the fixture works
|
|
67
67
|
|
|
68
|
-
The fixture is a projection of
|
|
68
|
+
The fixture is a projection of one selected persisted Session generation produced by running the real agent once — this plugin does not record. The snapshot harness supplies the numerically highest canonical parent path (`<scenario>/session.jsonl` for v0 or `<scenario>/session.vN.jsonl` for a positive generation), and validates filename/header agreement before replay. The fixture keeps the header and every event payload but omits body `seq`/`time` envelopes (`seq0`/`time0` for historical packed rows). Replay supplies contiguous sequences and deterministic timestamps, restores typed values replaced by snapshot tokens, rejects partial or mixed envelopes, decodes the complete physical artifact through the build-static Session format catalog, and migrates historical input in memory before it exposes events or the inherited cut; current input takes direct restoration. For a projected v0 header only, an absent `delegationDepth` denotes `0`. The parser never rewrites or renames the fixture. Runtime persistence continues to write complete logs. Replay expands the compact stream on each current-view `assistant/message` or `assistant/attempt`, so a recorded fixture replays the same logical stream the live model produced. A fixture may carry its `request/header` content tokenized to `{{system}}`/`{{tools}}`; replay materializes validation-only values, while derivation reads only Assistant settlements, marked summary events, and Session metadata. Every replay and comparison fixture must pass the same content-only catalog validation; replay never repairs a refused artifact.
|
|
69
69
|
|
|
70
70
|
### Nested agents
|
|
71
71
|
|
|
72
|
-
A scenario where a parent agent delegates to in-process subagents records one
|
|
72
|
+
A scenario where a parent agent delegates to in-process subagents records one role per Session: parent `session[.vN].jsonl`, then contiguous children `session.<ordinal>[.vN].jsonl`. The snapshot harness supplies only the highest generation of each role. Live Session ids are freshly random each run, so replay binds each live Session to a recorded script by first-call order: the first live Session to make a model call claims the parent script, the next new Session the next child script, and so on, with each Session advancing its own cursor. More distinct live Sessions than recorded scripts fails loud.
|
|
73
73
|
|
|
74
74
|
### Failure modes and overrides
|
|
75
75
|
|
|
76
76
|
When replay serves `deepseek-official` with `ctx.deepseekLlmApiExtensions`, it prepares and accepts those fields after selecting a valid script entry and before yielding the first chunk. This mirrors the live adapter's post-2xx commit point, so durable acceptance watermarks and SDK event notifications behave the same in recording and replay. Replay supplies a synthetic `{ messages: [] }` base body: it proves acceptance side effects, not prepared field bytes.
|
|
77
77
|
|
|
78
|
-
Two failure modes are not reconstructable from
|
|
78
|
+
Two failure modes are not reconstructable from a durable Assistant settlement alone: a pure throw before any chunk has no exception-bearing stream member, and a cancel/hang requires nontermination rather than a replayed finite prefix. A scenario that needs those supplies an optional sidecar (`<scenario>/replay.override.json`) that either replaces the derived script with a bare `ReplayEntry[]` or augments it with `{ patches: [{ at, entry }] }`, which keeps every derived call and swaps the named 0-based call indexes; `at` equal to the derived length appends the retry attempt after an injected transient throw. A `throw` entry accepts DeepSeek request extensions when it has prefix chunks; a zero-chunk throw defaults to pre-2xx non-acceptance and may set `accepted: true` for a post-2xx failure. A `hang` entry may name `readyFile`, which replay writes before waiting for cancellation so an external driver can cancel deterministically.
|
|
79
79
|
|
|
80
80
|
### What can go wrong
|
|
81
81
|
|
|
@@ -95,13 +95,16 @@ This section explains the design of the replay plugin; the observable behavior i
|
|
|
95
95
|
|
|
96
96
|
### Design
|
|
97
97
|
|
|
98
|
-
Replay
|
|
98
|
+
Replay treats the selected projected Session generation as the fixture. One parser completes projected envelopes, validates and migrates the whole artifact through `sessionFormatCatalog`, and returns the current header, inherited cut, and event list as one result. `deriveReplayScript` expands each `assistant/message` or `assistant/attempt` stream in log order, so each durable settlement becomes one `chunks` entry; a non-empty stream without a `finish` chunk is the fingerprint of a thrown `stream()` and must be expressed through an override sidecar. A `compaction/summary` carrying `llmStreamCall: true` and a complete `rawOutput` replays as one canonical successful stream at that event's position. Scripted strings may embed `{{fromRequest:<regex>}}`; at stream time each placeholder resolves against the live request's string leaves, taking the pattern's last match and its first capture group (or the whole match) in place.
|
|
99
|
+
|
|
100
|
+
The committed-corpus test discovers every versioned `session*.jsonl` under `snapshots/`, `packages/`, and `scripts/snapshots/python-sdk-single-exe/`. Every artifact must restore to the current view through the real catalog. Any refusal fails with the artifact path, so the fixture owner must correct the invalid relationship before replay or comparison.
|
|
99
101
|
|
|
100
102
|
### Source map
|
|
101
103
|
|
|
102
104
|
| File | Role |
|
|
103
105
|
|---|---|
|
|
104
106
|
| [`src/index.ts`](src/index.ts) | Types, fixture derivation, override validation, placeholder resolution, session binding, `installLlmReplay`, and the plugin export |
|
|
107
|
+
| [`tests/session-format-corpus.spec.ts`](tests/session-format-corpus.spec.ts) | Complete committed-generation restoration burn-in; every refusal is a failure |
|
|
105
108
|
| — | No runtime invariant companion is published; this test-only adapter consumes a fixed replay script; its stream grammar is checked by the LLM companion and fixture derivation tests. |
|
|
106
109
|
|
|
107
110
|
### Binding and stream flow
|
package/README.zh.md
CHANGED
|
@@ -9,7 +9,7 @@ kind: "package-reference"
|
|
|
9
9
|
|
|
10
10
|
## 概述
|
|
11
11
|
|
|
12
|
-
`dsh-llm-replay` 让快照测试无需 API 密钥即可运行:它安装一个回放 LLM(大语言模型)适配器,从已记录的会话 JSONL fixture(测试前置数据)重建模型流,使测试针对固定 transcript(文本记录)启动真实 agent(智能体)。fixture
|
|
12
|
+
`dsh-llm-replay` 让快照测试无需 API 密钥即可运行:它安装一个回放 LLM(大语言模型)适配器,从已记录的会话 JSONL fixture(测试前置数据)重建模型流,使测试针对固定 transcript(文本记录)启动真实 agent(智能体)。fixture 是持久化会话日志的投影——每个 `assistant/message` 或 `assistant/attempt` 都嵌入一次模型调用的 stream,显式标记的本地压缩(compaction)调用则回放为一条规范流。`replay.override.json` 伴随文件覆盖 settlement 无法重建的情况:任何分片之前就抛出、取消/挂起,或注入重试。实时会话按首次调用顺序绑定到已记录脚本,因此父会话与 subagent 场景各自获得自己的脚本。它是 ACP 与 headless 快照套件以及 Web 浏览器 e2e 流水线的模型来源。
|
|
13
13
|
|
|
14
14
|
## 目录
|
|
15
15
|
|
|
@@ -55,7 +55,7 @@ kind: "package-reference"
|
|
|
55
55
|
|
|
56
56
|
| 字段 | 默认值 | 含义 |
|
|
57
57
|
|---|---|---|
|
|
58
|
-
| `file` | `$DSH_SNAPSHOT_FILE` |
|
|
58
|
+
| `file` | `$DSH_SNAPSHOT_FILE` | 选定 primary fixture 路径:v0 为 `session.jsonl`,正 generation 为 `session.vN.jsonl`;必需(config 或 env) |
|
|
59
59
|
| `overrideFile` | `$DSH_SNAPSHOT_OVERRIDE` | 主会话的可选 `ReplayOverrideDoc` 伴随文件 |
|
|
60
60
|
| `childFiles` | `$DSH_SNAPSHOT_CHILD_FILES` | 嵌套场景中已记录的 subagent 子会话日志 |
|
|
61
61
|
| `providers` | 无 | 可选的仅回放提供方与模型目录;模型可声明 `contextWindow`、文本/图片模态,以及图片模型使用的正整数 `imageRequestTokens`;非法值会在加载时失败,路由绝不执行提供方 I/O |
|
|
@@ -65,17 +65,17 @@ kind: "package-reference"
|
|
|
65
65
|
|
|
66
66
|
### fixture 的工作方式
|
|
67
67
|
|
|
68
|
-
fixture 是运行一次真实 agent
|
|
68
|
+
fixture 是运行一次真实 agent 所产生的一份选定持久化 Session generation 投影,本插件不录制。snapshot harness 会提供数值最高的规范 parent 路径(v0 为 `<scenario>/session.jsonl`,正 generation 为 `<scenario>/session.vN.jsonl`),并在 replay 前校验文件名与 header 一致。fixture 保留 header 与每个事件 payload,但省略正文的 `seq`/`time` envelope(历史 packed row 使用 `seq0`/`time0`)。replay 补充连续序号与确定性 timestamp,恢复被 snapshot token 替换的类型化值,拒绝不完整或混合 envelope,通过构建期静态 Session 格式 catalog 解码完整物理产物,并在公开事件或继承 cut 前于内存中迁移历史输入;当前输入直接 restore。仅对投影 v0 header,缺失的 `delegationDepth` 表示 `0`。parser 从不重写或重命名 fixture。runtime persistence 继续写入完整日志。replay 会展开当前视图中每个 `assistant/message` 或 `assistant/attempt` 的紧凑 stream,因此已记录 fixture 会 replay 与在线模型产生的相同逻辑流。fixture 的 `request/header` 内容可能 token 化为 `{{system}}`/`{{tools}}`;replay 会物化仅用于校验的值,而派生只读取 Assistant settlement、带标记的 summary 事件与 Session metadata。每个 replay 与 comparison fixture 都必须通过同一个只基于内容的 catalog 校验;replay 绝不修复被拒绝的产物。
|
|
69
69
|
|
|
70
70
|
### 嵌套 agent
|
|
71
71
|
|
|
72
|
-
|
|
72
|
+
parent agent 委托给进程内 subagent 的场景会为每个 Session 记录一个角色:parent 为 `session[.vN].jsonl`,随后是连续 child `session.<ordinal>[.vN].jsonl`。snapshot harness 只提供每个角色的最高 generation。live Session id 每次运行都会重新随机生成,因此 replay 按首次调用顺序把每个 live Session 绑定到已记录脚本:第一个发起模型调用的 live Session 取得 parent 脚本,下一个新 Session 取得下一条 child 脚本,依此类推,每个 Session 分别推进自己的 cursor。不同 live Session 数量超过已记录脚本数时会明确报错。
|
|
73
73
|
|
|
74
74
|
### 失败模式与覆盖
|
|
75
75
|
|
|
76
76
|
当回放在带有 `ctx.deepseekLlmApiExtensions` 的组合中服务 `deepseek-official` 时,它会在选择有效脚本条目后、产生首个分片前准备并接受这些字段。这与实时适配器的 2xx 后提交点一致,因此持久接受水位与 SDK 事件通知在录制和回放中行为相同。回放提供合成 `{ messages: [] }` 基础 body:它证明接受副作用,而非准备后的字段字节。
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
有两种失败模式无法仅根据持久 Assistant settlement 重建:任何 chunk 之前的纯 throw 没有携带异常的 stream member,而 cancel/hang 需要的是不终止语义,不能用有限前缀回放。需要这些行为的场景可提供可选伴随文件(`<scenario>/replay.override.json`):它用裸 `ReplayEntry[]` 替换派生脚本,或用 `{ patches: [{ at, entry }] }` 增补——保留所有派生调用,只替换指定的从 0 开始计数的调用索引;当 `at` 等于派生长度时,则在注入瞬态异常后的重试位置追加。有前缀分片的 `throw` 条目会接受 DeepSeek 请求扩展;零分片 throw 默认表示 2xx 前未接受,也可设 `accepted: true` 表示 2xx 后无分片失败。`hang` 条目可以指定 `readyFile`,回放在等待取消前写入它,使外部 driver 可以确定性取消。
|
|
79
79
|
|
|
80
80
|
### 可能出什么问题
|
|
81
81
|
|
|
@@ -95,13 +95,16 @@ fixture 是运行一次真实 agent 所产生的持久化会话日志(`<scenar
|
|
|
95
95
|
|
|
96
96
|
### 设计
|
|
97
97
|
|
|
98
|
-
|
|
98
|
+
replay 把选定的投影 Session generation 视为 fixture。一个 parser 补全投影 envelope,通过 `sessionFormatCatalog` 校验并迁移完整产物,再以一个结果返回当前 header、继承 cut 与事件列表。`deriveReplayScript` 按日志顺序展开每个 `assistant/message` 或 `assistant/attempt` stream,因此每个持久 settlement 都成为一条 `chunks` entry;非空 stream 缺少 `finish` chunk 是 `stream()` 抛出异常的 fingerprint,必须通过 override sidecar 表达。携带 `llmStreamCall: true` 与完整 `rawOutput` 的 `compaction/summary` 会在该事件位置 replay 为一条规范成功 stream。脚本字符串可以内嵌 `{{fromRequest:<regex>}}`;stream 输出时每个 placeholder 针对 live request 的 string leaf 解析,取该 pattern 的最后一次 match,用其第一个 capture group(无 capture group 时用整个 match)原位替换。
|
|
99
|
+
|
|
100
|
+
已提交语料测试会发现 `snapshots/`、`packages/` 与 `scripts/snapshots/python-sdk-single-exe/` 下每个带版本的 `session*.jsonl`。每个产物都必须通过真实 catalog 还原为当前视图。任何拒绝都会携带产物路径并使测试失败,因此 fixture owner 必须在 replay 或比较前修正无效关系。
|
|
99
101
|
|
|
100
102
|
### 源码地图
|
|
101
103
|
|
|
102
104
|
| 文件 | 职责 |
|
|
103
105
|
|---|---|
|
|
104
106
|
| [`src/index.ts`](src/index.ts) | 类型、fixture 派生、override 校验、占位符解析、会话绑定、`installLlmReplay` 与插件导出 |
|
|
107
|
+
| [`tests/session-format-corpus.spec.ts`](tests/session-format-corpus.spec.ts) | 完整已提交 generation restore burn-in;任何拒绝都是失败 |
|
|
105
108
|
| — | 不发布运行时不变式伴生入口;流语法由 LLM 伴生插件与派生测试检验。 |
|
|
106
109
|
|
|
107
110
|
### 绑定与流式流程
|
package/lib/index.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { delimiter } from "node:path";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { SESSION_FORMAT_VERSION, SessionLogOffset } from "@deepseek-ai/dsh-session";
|
|
4
|
+
import { SessionFormatUnsupportedMigrationError, sessionFormatCatalog } from "@deepseek-ai/dsh-session-format-catalog";
|
|
5
|
+
import { LlmAdapter, LlmError, ReasoningEffortId, expandAssistantStream, requestImageHandleText, resolveRetryPolicy } from "@deepseek-ai/dsh-llm";
|
|
5
6
|
import { assertNever } from "@deepseek-ai/dsh-util-values";
|
|
6
7
|
//#region lib/types/index.js
|
|
7
8
|
/**
|
|
8
9
|
* Keyless snapshot-test LLM replay. It derives one model-call script per
|
|
9
|
-
* recorded session from
|
|
10
|
+
* recorded session from v2 embedded Assistant streams and explicitly marked local
|
|
10
11
|
* compaction calls, then binds fresh live sessions to parent/child scripts by
|
|
11
12
|
* first-call order. Throw and hang cases require an explicit override because
|
|
12
13
|
* a session log cannot reconstruct them alone.
|
|
@@ -17,26 +18,30 @@ const PACKED_CHUNK_ROW_TYPES = new Set([
|
|
|
17
18
|
"reasoning-chunks",
|
|
18
19
|
"tool-call-chunks"
|
|
19
20
|
]);
|
|
21
|
+
if (sessionFormatCatalog.currentVersion !== SESSION_FORMAT_VERSION) throw new Error(`llm-replay: format catalog v${sessionFormatCatalog.currentVersion} does not match Session v${SESSION_FORMAT_VERSION}`);
|
|
20
22
|
/**
|
|
21
|
-
* Parse a session `.jsonl` buffer into
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* header is skipped; malformed lines fail loud.
|
|
23
|
+
* Parse a projected session `.jsonl` buffer into current events. The first
|
|
24
|
+
* non-empty line is the physical header. Body rows either all carry complete
|
|
25
|
+
* persistence envelopes or all omit them; projected rows receive deterministic
|
|
26
|
+
* dense sequences and zero timestamps. The build-static format catalog then
|
|
27
|
+
* decodes and migrates the complete artifact before this function returns.
|
|
27
28
|
* @param text - the raw `.jsonl` file contents.
|
|
28
|
-
* @returns every
|
|
29
|
+
* @returns every migrated current event, in log order.
|
|
29
30
|
*/
|
|
30
31
|
function parseSessionLog(text) {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
return parseSessionFixture(text).events;
|
|
33
|
+
}
|
|
34
|
+
/** Parse, complete, decode, and migrate one projected snapshot artifact without writing its source. */
|
|
35
|
+
function parseSessionFixture(text) {
|
|
36
|
+
let headerLineNumber;
|
|
37
|
+
let sourceHeader;
|
|
38
|
+
let restore;
|
|
39
|
+
const rowLines = [];
|
|
40
|
+
const eventLines = [];
|
|
41
|
+
let bodyKind;
|
|
42
|
+
let nextSeq = 0;
|
|
34
43
|
for (const [index, line] of text.split(/\r?\n/).entries()) {
|
|
35
44
|
if (line.trim().length === 0) continue;
|
|
36
|
-
if (!headerSkipped) {
|
|
37
|
-
headerSkipped = true;
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
45
|
let value;
|
|
41
46
|
try {
|
|
42
47
|
value = JSON.parse(line);
|
|
@@ -44,46 +49,305 @@ function parseSessionLog(text) {
|
|
|
44
49
|
throw new Error(`session snapshot line ${index + 1} contains invalid JSON`, { cause: error });
|
|
45
50
|
}
|
|
46
51
|
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`session snapshot line ${index + 1} must be a JSON object`);
|
|
47
|
-
const
|
|
52
|
+
const lineNumber = index + 1;
|
|
53
|
+
const recordValue = value;
|
|
54
|
+
if (restore === void 0) {
|
|
55
|
+
headerLineNumber = lineNumber;
|
|
56
|
+
sourceHeader = recordValue;
|
|
57
|
+
try {
|
|
58
|
+
restore = sessionFormatCatalog.createRestore(normalizeProjectedHeader(recordValue), {
|
|
59
|
+
recovery: "strict",
|
|
60
|
+
validation: "current"
|
|
61
|
+
});
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw fixtureFormatError(error, lineNumber, [], []);
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const record = normalizeProjectedRow(recordValue);
|
|
48
68
|
const packed = PACKED_CHUNK_ROW_TYPES.has(record.type);
|
|
49
69
|
const seqKey = packed ? "seq0" : "seq";
|
|
50
70
|
const timeKey = packed ? "time0" : "time";
|
|
51
|
-
|
|
52
|
-
if (
|
|
53
|
-
|
|
71
|
+
const hasSeq = Object.hasOwn(record, seqKey);
|
|
72
|
+
if (hasSeq !== Object.hasOwn(record, timeKey)) throw new Error(`session snapshot line ${lineNumber} must contain both ${seqKey} and ${timeKey}, or neither`);
|
|
73
|
+
const currentKind = hasSeq ? "complete" : "projected";
|
|
74
|
+
if (bodyKind !== void 0 && currentKind !== bodyKind) throw new Error(`session snapshot line ${lineNumber} cannot mix projected and complete body rows`);
|
|
75
|
+
bodyKind = currentKind;
|
|
76
|
+
if (currentKind === "projected") {
|
|
77
|
+
record[seqKey] = nextSeq;
|
|
78
|
+
record[timeKey] = 0;
|
|
79
|
+
}
|
|
80
|
+
const cardinality = physicalRowCardinality(record);
|
|
81
|
+
rowLines.push(lineNumber);
|
|
82
|
+
eventLines.push(...Array.from({ length: cardinality }, () => lineNumber));
|
|
83
|
+
nextSeq += cardinality;
|
|
54
84
|
try {
|
|
55
|
-
|
|
56
|
-
decoded = decodeStorageRecord(record);
|
|
85
|
+
restore.decodeRow(record);
|
|
57
86
|
} catch (error) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
87
|
+
throw fixtureFormatError(error, headerLineNumber, rowLines, eventLines, rowLines.length - 1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (restore === void 0 || sourceHeader === void 0 || headerLineNumber === void 0) throw new Error("session snapshot must start with a session header");
|
|
91
|
+
try {
|
|
92
|
+
return parsedSessionFixture(restore.finish(), sourceHeader);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
throw fixtureFormatError(error, headerLineNumber, rowLines, eventLines);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/** Materialize the common replay view from a migrated artifact. */
|
|
98
|
+
function parsedSessionFixture(artifact, sourceHeader) {
|
|
99
|
+
return {
|
|
100
|
+
id: artifact.header.id,
|
|
101
|
+
createdAt: artifact.header.createdAt,
|
|
102
|
+
inheritedEventCount: SessionLogOffset(artifact.inheritedEventCount),
|
|
103
|
+
events: [...artifact.events],
|
|
104
|
+
artifact,
|
|
105
|
+
sourceHeader
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Convert one persisted or projected snapshot fixture to the current physical format in memory for expected-output comparison.
|
|
110
|
+
* Projected cwd tokens remain tokens so the ordinary snapshot normalizer can compare them with a fresh run.
|
|
111
|
+
* @param text - one complete Session fixture.
|
|
112
|
+
* @returns current-format JSONL with complete event envelopes; the input string and source file remain unchanged.
|
|
113
|
+
*/
|
|
114
|
+
function prepareSessionSnapshotFixtureForComparison(text) {
|
|
115
|
+
return encodeCurrentSessionSnapshotFixture(text, parseSessionFixture(text));
|
|
116
|
+
}
|
|
117
|
+
/** Read one headless or SDK event-notification wrapper. */
|
|
118
|
+
function wrappedSessionEvent(row) {
|
|
119
|
+
if (row["type"] === "session_event" && typeof row["sessionId"] === "string" && row["event"] !== null && typeof row["event"] === "object" && !Array.isArray(row["event"])) return {
|
|
120
|
+
sessionId: row["sessionId"],
|
|
121
|
+
event: row["event"],
|
|
122
|
+
replace: (event) => ({
|
|
123
|
+
...row,
|
|
124
|
+
event
|
|
125
|
+
})
|
|
126
|
+
};
|
|
127
|
+
const params = row["params"];
|
|
128
|
+
if (row["method"] !== "session.event" || params === null || typeof params !== "object" || Array.isArray(params)) return;
|
|
129
|
+
const record = params;
|
|
130
|
+
if (typeof record["sessionId"] !== "string" || record["event"] === null || typeof record["event"] !== "object" || Array.isArray(record["event"])) return;
|
|
131
|
+
return {
|
|
132
|
+
sessionId: record["sessionId"],
|
|
133
|
+
event: record["event"],
|
|
134
|
+
replace: (event) => ({
|
|
135
|
+
...row,
|
|
136
|
+
params: {
|
|
137
|
+
...record,
|
|
138
|
+
event
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
/** Append one migrated event to the output assigned to its v1 source row. */
|
|
144
|
+
function assignMigratedEvent(assigned, rowIndex, event) {
|
|
145
|
+
assigned.set(rowIndex, [...assigned.get(rowIndex) ?? [], event]);
|
|
146
|
+
}
|
|
147
|
+
/** Restore fixture tokens materialized only to satisfy released-format validation. */
|
|
148
|
+
function restoreProjectedRequestHeader(target, source) {
|
|
149
|
+
if (target["type"] !== "request/header" || source["type"] !== "request/header") return target;
|
|
150
|
+
const targetData = target["data"];
|
|
151
|
+
const sourceData = source["data"];
|
|
152
|
+
const targetHeader = targetData["header"];
|
|
153
|
+
const sourceTools = sourceData["header"]["tools"];
|
|
154
|
+
if (sourceTools !== "{{tools}}") return target;
|
|
155
|
+
return {
|
|
156
|
+
...target,
|
|
157
|
+
data: {
|
|
158
|
+
...targetData,
|
|
159
|
+
header: {
|
|
160
|
+
...targetHeader,
|
|
161
|
+
tools: sourceTools
|
|
162
|
+
}
|
|
61
163
|
}
|
|
62
|
-
|
|
63
|
-
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/** Omit delivery cursors whose numeric value depends on the source Session generation. */
|
|
167
|
+
function normalizeWrappedEventProvenance(event) {
|
|
168
|
+
if (event["type"] !== "session-log-deepseek/delivery-accepted") return event;
|
|
169
|
+
const data = event["data"];
|
|
170
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) return event;
|
|
171
|
+
const normalized = { ...data };
|
|
172
|
+
delete normalized["sessionFormatVersion"];
|
|
173
|
+
delete normalized["throughSeq"];
|
|
174
|
+
return {
|
|
175
|
+
...event,
|
|
176
|
+
data: normalized
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/** Migrate one session's contiguous v1 notification tail and align its surviving rows. */
|
|
180
|
+
function migrateWrappedEventGroup(entries) {
|
|
181
|
+
const first = entries[0];
|
|
182
|
+
if (!entries.some((entry) => entry.event["type"] === "assistant/chunk")) return /* @__PURE__ */ new Map();
|
|
183
|
+
const firstSeq = first.event["seq"];
|
|
184
|
+
if (!Number.isSafeInteger(firstSeq) || firstSeq < 0) throw new Error("session event comparison requires a non-negative first seq");
|
|
185
|
+
for (const [index, entry] of entries.entries()) if (entry.event["seq"] !== firstSeq + index || typeof entry.event["type"] !== "string") throw new Error(`session event comparison requires a contiguous event tail for ${first.sessionId}`);
|
|
186
|
+
const prefix = Array.from({ length: firstSeq }, (_, seq) => ({
|
|
187
|
+
type: "feedback/record",
|
|
188
|
+
seq,
|
|
189
|
+
time: 0,
|
|
190
|
+
data: { text: `comparison prefix ${String(seq)}` }
|
|
191
|
+
}));
|
|
192
|
+
const restore = sessionFormatCatalog.createRestore({
|
|
193
|
+
type: "session",
|
|
194
|
+
version: 1,
|
|
195
|
+
id: first.sessionId,
|
|
196
|
+
createdAt: 0,
|
|
197
|
+
delegationDepth: 0
|
|
198
|
+
}, {
|
|
199
|
+
recovery: "strict",
|
|
200
|
+
validation: "current"
|
|
201
|
+
});
|
|
202
|
+
for (const event of prefix) restore.decodeRow(event);
|
|
203
|
+
for (const entry of entries) restore.decodeRow(normalizeProjectedRow(entry.event));
|
|
204
|
+
const migrated = restore.finish().events.slice(prefix.length);
|
|
205
|
+
const assigned = /* @__PURE__ */ new Map();
|
|
206
|
+
let migratedIndex = 0;
|
|
207
|
+
let lastChunkRow;
|
|
208
|
+
for (const entry of entries) {
|
|
209
|
+
if (entry.event["type"] === "assistant/chunk") {
|
|
210
|
+
lastChunkRow = entry.rowIndex;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
while (migrated[migratedIndex]?.["type"] === "assistant/attempt") {
|
|
214
|
+
assignMigratedEvent(assigned, lastChunkRow, migrated[migratedIndex]);
|
|
215
|
+
migratedIndex += 1;
|
|
216
|
+
}
|
|
217
|
+
const next = migrated[migratedIndex];
|
|
218
|
+
assignMigratedEvent(assigned, entry.rowIndex, restoreProjectedRequestHeader(next, entry.event));
|
|
219
|
+
migratedIndex += 1;
|
|
220
|
+
}
|
|
221
|
+
while (migrated[migratedIndex]?.["type"] === "assistant/attempt") {
|
|
222
|
+
assignMigratedEvent(assigned, lastChunkRow, migrated[migratedIndex]);
|
|
223
|
+
migratedIndex += 1;
|
|
224
|
+
}
|
|
225
|
+
return assigned;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Project v1 session-event notifications to current settlement cardinality in memory.
|
|
229
|
+
* Non-event protocol rows retain their exact positions, and current v2 input passes through.
|
|
230
|
+
* @param text - headless `session_event` or SDK `session.event` JSONL.
|
|
231
|
+
* @returns comparison JSONL using current Session events without modifying its source file.
|
|
232
|
+
*/
|
|
233
|
+
function prepareSessionEventNotificationsForComparison(text) {
|
|
234
|
+
const trailingNewline = text.endsWith("\n");
|
|
235
|
+
const rows = text.split("\n").filter((line) => line.trim().length > 0).map((line, index) => {
|
|
236
|
+
const value = JSON.parse(line);
|
|
237
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error(`session event comparison line ${String(index + 1)} must be an object`);
|
|
238
|
+
return value;
|
|
239
|
+
});
|
|
240
|
+
const entries = rows.flatMap((row, rowIndex) => {
|
|
241
|
+
const wrapped = wrappedSessionEvent(row);
|
|
242
|
+
return wrapped === void 0 ? [] : [{
|
|
243
|
+
...wrapped,
|
|
244
|
+
rowIndex
|
|
245
|
+
}];
|
|
246
|
+
});
|
|
247
|
+
const groups = [];
|
|
248
|
+
for (const entry of entries) {
|
|
249
|
+
const nextSeq = entry.event["seq"];
|
|
250
|
+
const group = groups.findLast((candidate) => {
|
|
251
|
+
const previous = candidate.at(-1);
|
|
252
|
+
const previousSeq = previous?.event["seq"];
|
|
253
|
+
return previous?.sessionId === entry.sessionId && Number.isSafeInteger(previousSeq) && Number.isSafeInteger(nextSeq) && nextSeq === previousSeq + 1;
|
|
254
|
+
});
|
|
255
|
+
if (group === void 0) groups.push([entry]);
|
|
256
|
+
else group.push(entry);
|
|
64
257
|
}
|
|
65
|
-
|
|
258
|
+
const assigned = /* @__PURE__ */ new Map();
|
|
259
|
+
for (const group of groups) for (const [rowIndex, events] of migrateWrappedEventGroup(group)) assigned.set(rowIndex, events);
|
|
260
|
+
const output = rows.flatMap((row, rowIndex) => {
|
|
261
|
+
const wrapped = wrappedSessionEvent(row);
|
|
262
|
+
if (wrapped === void 0) return [row];
|
|
263
|
+
const migrated = assigned.get(rowIndex);
|
|
264
|
+
if (migrated === void 0) return wrapped.event["type"] === "assistant/chunk" ? [] : [wrapped.replace(normalizeWrappedEventProvenance(wrapped.event))];
|
|
265
|
+
return migrated.map((event) => wrapped.replace(normalizeWrappedEventProvenance(event)));
|
|
266
|
+
}).map((row) => JSON.stringify(row)).join("\n");
|
|
267
|
+
return trailingNewline ? `${output}\n` : output;
|
|
268
|
+
}
|
|
269
|
+
/** Encode one migrated fixture while retaining a projected cwd token. */
|
|
270
|
+
function encodeCurrentSessionSnapshotFixture(text, parsed) {
|
|
271
|
+
const header = { ...sessionFormatCatalog.encodeCurrentHeader(parsed.artifact.header, parsed.artifact.inheritedEventCount) };
|
|
272
|
+
const sourceCwd = parsed.sourceHeader["cwd"];
|
|
273
|
+
if (typeof sourceCwd === "string" && /^\{\{cwd\}\}(?:\/|$)/.test(sourceCwd)) header["cwd"] = sourceCwd;
|
|
274
|
+
const output = [JSON.stringify(header), ...parsed.artifact.events.map((event) => JSON.stringify(sessionFormatCatalog.encodeCurrentEvent(event)))].join("\n");
|
|
275
|
+
return text.endsWith("\n") ? `${output}\n` : output;
|
|
276
|
+
}
|
|
277
|
+
/** Restore typed request-header values replaced by snapshot sidecar tokens. */
|
|
278
|
+
function normalizeProjectedRow(source) {
|
|
279
|
+
const record = { ...source };
|
|
280
|
+
if (record["type"] !== "request/header") return record;
|
|
281
|
+
const data = record["data"];
|
|
282
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) return record;
|
|
283
|
+
const header = data["header"];
|
|
284
|
+
if (header === null || typeof header !== "object" || Array.isArray(header)) return record;
|
|
285
|
+
const tools = header["tools"];
|
|
286
|
+
let materializedTools;
|
|
287
|
+
if (tools === "{{tools}}") materializedTools = [];
|
|
288
|
+
else if (Array.isArray(tools) && tools.every((tool) => typeof tool === "string" && tool.length > 0)) materializedTools = tools.map((name) => ({
|
|
289
|
+
name,
|
|
290
|
+
description: "",
|
|
291
|
+
parameters: {}
|
|
292
|
+
}));
|
|
293
|
+
else return record;
|
|
294
|
+
record["data"] = {
|
|
295
|
+
...data,
|
|
296
|
+
header: {
|
|
297
|
+
...header,
|
|
298
|
+
tools: materializedTools
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
return record;
|
|
302
|
+
}
|
|
303
|
+
/** Materialize fixture-only header omissions and tokens before physical validation. */
|
|
304
|
+
function normalizeProjectedHeader(header) {
|
|
305
|
+
const normalized = { ...header };
|
|
306
|
+
if (normalized["version"] === 0 && !Object.hasOwn(header, "delegationDepth")) normalized["delegationDepth"] = 0;
|
|
307
|
+
if (typeof header["cwd"] === "string" && /^\{\{cwd\}\}(?:\/|$)/.test(header["cwd"])) normalized["cwd"] = header["cwd"].replace("{{cwd}}", "/dsh-snapshot-cwd");
|
|
308
|
+
return normalized;
|
|
309
|
+
}
|
|
310
|
+
/** Return how many logical events one physical row contributes for deterministic seq completion. */
|
|
311
|
+
function physicalRowCardinality(row) {
|
|
312
|
+
if (!PACKED_CHUNK_ROW_TYPES.has(row["type"])) return 1;
|
|
313
|
+
const data = row["data"];
|
|
314
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) return 1;
|
|
315
|
+
const payload = data[row["type"] === "tool-call-chunks" ? "args" : "texts"];
|
|
316
|
+
return Array.isArray(payload) && payload.length > 0 ? payload.length : 1;
|
|
317
|
+
}
|
|
318
|
+
/** Attach the nearest physical source line while preserving unsupported-migration classification. */
|
|
319
|
+
function fixtureFormatError(error, headerLine, rowLines, eventLines, physicalRow) {
|
|
320
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
321
|
+
const locationDetail = error instanceof Error && error.cause instanceof Error ? error.cause.message : detail;
|
|
322
|
+
const storedRow = /^released Session row (\d+)/.exec(locationDetail);
|
|
323
|
+
const event = /Session event (\d+)/.exec(locationDetail) ?? / at seq (\d+)/.exec(locationDetail) ?? /inherited Session cut (\d+)/.exec(locationDetail);
|
|
324
|
+
let line;
|
|
325
|
+
if (physicalRow !== void 0) line = rowLines[physicalRow];
|
|
326
|
+
else if (storedRow !== null) line = rowLines[Number(storedRow[1])] ?? headerLine;
|
|
327
|
+
else if (event === null) line = headerLine;
|
|
328
|
+
else line = eventLines[Number(event[1])] ?? headerLine;
|
|
329
|
+
const message = `session snapshot line ${line}: ${detail}`;
|
|
330
|
+
if (error instanceof SessionFormatUnsupportedMigrationError) return new SessionFormatUnsupportedMigrationError(message, { cause: error });
|
|
331
|
+
return new Error(message, { cause: error });
|
|
66
332
|
}
|
|
67
333
|
/**
|
|
68
334
|
* Read replay identity, ordering, and fork-seed facts from the JSONL header.
|
|
69
335
|
*
|
|
70
|
-
* @param text - the raw `.jsonl` file contents
|
|
71
|
-
* @returns the header's `id`, `createdAt`, and inherited-event count
|
|
336
|
+
* @param text - the raw `.jsonl` file contents; the complete artifact is validated and migrated.
|
|
337
|
+
* @returns the migrated header's `id`, `createdAt`, and exact inherited-event count.
|
|
72
338
|
*/
|
|
73
339
|
function parseSessionHeader(text) {
|
|
74
|
-
const
|
|
75
|
-
const parsed = JSON.parse(firstLine);
|
|
340
|
+
const parsed = parseSessionFixture(text);
|
|
76
341
|
return {
|
|
77
|
-
id:
|
|
78
|
-
createdAt:
|
|
79
|
-
inheritedEventCount:
|
|
342
|
+
id: parsed.id,
|
|
343
|
+
createdAt: parsed.createdAt,
|
|
344
|
+
inheritedEventCount: parsed.inheritedEventCount
|
|
80
345
|
};
|
|
81
346
|
}
|
|
82
347
|
/**
|
|
83
348
|
* Reconstruct the per-`stream()` replay script from a recorded session log.
|
|
84
349
|
*
|
|
85
|
-
*
|
|
86
|
-
* to detect an unterminated prior call. A `compaction/summary` explicitly marked
|
|
350
|
+
* Reads one embedded stream from each Assistant settlement. A `compaction/summary` explicitly marked
|
|
87
351
|
* as one local LLM-stream call becomes a canonical successful stream from its
|
|
88
352
|
* complete `rawOutput` at the summary's log position. A
|
|
89
353
|
* missing assistant terminator means the live stream threw, so derivation
|
|
@@ -94,8 +358,6 @@ function parseSessionHeader(text) {
|
|
|
94
358
|
*/
|
|
95
359
|
function deriveReplayScript(events) {
|
|
96
360
|
const script = [];
|
|
97
|
-
let currentKey;
|
|
98
|
-
let current = [];
|
|
99
361
|
const close = (key, chunks) => {
|
|
100
362
|
if (chunks.length === 0) return;
|
|
101
363
|
if (chunks[chunks.length - 1]?.type !== "finish") throw new Error(`llm-replay: model call ${key} ended without a finish chunk (a thrown stream); this scenario needs a replay.override.json sidecar`);
|
|
@@ -106,9 +368,6 @@ function deriveReplayScript(events) {
|
|
|
106
368
|
};
|
|
107
369
|
for (const event of events) {
|
|
108
370
|
if (event.type === "compaction/summary") {
|
|
109
|
-
close(currentKey, current);
|
|
110
|
-
currentKey = void 0;
|
|
111
|
-
current = [];
|
|
112
371
|
const persisted = event.data;
|
|
113
372
|
if (persisted.llmStreamCall === true) {
|
|
114
373
|
if (persisted.rawOutput === void 0) throw new Error("llm-replay: compaction/summary marks an LLM stream call without rawOutput");
|
|
@@ -140,19 +399,10 @@ function deriveReplayScript(events) {
|
|
|
140
399
|
}
|
|
141
400
|
continue;
|
|
142
401
|
}
|
|
143
|
-
if (event.type !== "assistant/
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
if (current.length > 0 && key !== currentKey) close(currentKey, current);
|
|
147
|
-
if (current.length === 0) currentKey = key;
|
|
148
|
-
current.push(chunk);
|
|
149
|
-
if (chunk.type === "finish") {
|
|
150
|
-
close(currentKey, current);
|
|
151
|
-
currentKey = void 0;
|
|
152
|
-
current = [];
|
|
153
|
-
}
|
|
402
|
+
if (event.type !== "assistant/message" && event.type !== "assistant/attempt") continue;
|
|
403
|
+
const chunks = expandAssistantStream(event.data.stream).map((member) => member.chunk);
|
|
404
|
+
close(`${String(event.data.turn)}/${String(event.data.step)}`, chunks);
|
|
154
405
|
}
|
|
155
|
-
close(currentKey, current);
|
|
156
406
|
return script;
|
|
157
407
|
}
|
|
158
408
|
const REPLAY_CHUNK_TYPES = new Set([
|
|
@@ -346,10 +596,19 @@ function readOverrideDoc(value, file) {
|
|
|
346
596
|
* @returns the resolved primary-session script.
|
|
347
597
|
*/
|
|
348
598
|
function loadReplayScript(config) {
|
|
599
|
+
return resolveReplayScript(config, readPrimaryFixture(config));
|
|
600
|
+
}
|
|
601
|
+
/** Read a primary JSONL unless a whole-script sidecar intentionally occupies the same path. */
|
|
602
|
+
function readPrimaryFixture(config) {
|
|
603
|
+
if (!existsSync(config.file) || config.file === config.overrideFile) return void 0;
|
|
604
|
+
return parseSessionFixture(readFileSync(config.file, "utf8"));
|
|
605
|
+
}
|
|
606
|
+
/** Resolve an override or derive from one already validated and migrated fixture. */
|
|
607
|
+
function resolveReplayScript(config, fixture) {
|
|
349
608
|
if (config.overrideFile !== void 0 && existsSync(config.overrideFile)) {
|
|
350
609
|
const doc = readOverrideDoc(JSON.parse(readFileSync(config.overrideFile, "utf8")), config.overrideFile);
|
|
351
610
|
if (Array.isArray(doc)) return doc;
|
|
352
|
-
const script =
|
|
611
|
+
const script = deriveScriptFromFixture(config.file, fixture);
|
|
353
612
|
const derivedLength = script.length;
|
|
354
613
|
const seenIndexes = /* @__PURE__ */ new Set();
|
|
355
614
|
for (const patch of doc.patches) {
|
|
@@ -360,12 +619,12 @@ function loadReplayScript(config) {
|
|
|
360
619
|
}
|
|
361
620
|
return script;
|
|
362
621
|
}
|
|
363
|
-
return
|
|
622
|
+
return deriveScriptFromFixture(config.file, fixture);
|
|
364
623
|
}
|
|
365
|
-
/** Derive
|
|
366
|
-
function
|
|
367
|
-
if (
|
|
368
|
-
return deriveReplayScript(
|
|
624
|
+
/** Derive a script from an already migrated fixture, failing loud when it is absent. */
|
|
625
|
+
function deriveScriptFromFixture(file, fixture) {
|
|
626
|
+
if (fixture === void 0) throw new Error(`llm-replay: fixture not found: ${file} — run \`pnpm run test:snapshot:record\` first`);
|
|
627
|
+
return deriveReplayScript(fixture.events);
|
|
369
628
|
}
|
|
370
629
|
/**
|
|
371
630
|
* Load the primary and child scripts in bind order. Child derivation begins at
|
|
@@ -375,8 +634,9 @@ function deriveScriptFromFile(file) {
|
|
|
375
634
|
* @returns the primary script first, then the child scripts in bind order.
|
|
376
635
|
*/
|
|
377
636
|
function loadSessionScripts(config) {
|
|
378
|
-
const
|
|
379
|
-
const
|
|
637
|
+
const primaryFixture = readPrimaryFixture(config);
|
|
638
|
+
const primaryEntries = resolveReplayScript(config, primaryFixture);
|
|
639
|
+
const primaryHeader = primaryFixture ?? {
|
|
380
640
|
id: "",
|
|
381
641
|
createdAt: 0
|
|
382
642
|
};
|
|
@@ -389,12 +649,11 @@ function loadSessionScripts(config) {
|
|
|
389
649
|
const children = [];
|
|
390
650
|
for (const childFile of config.childFiles ?? []) {
|
|
391
651
|
if (!existsSync(childFile)) throw new Error(`llm-replay: child fixture not found: ${childFile} — re-record the scenario`);
|
|
392
|
-
const
|
|
393
|
-
const
|
|
394
|
-
const ownEvents = parseSessionLog(text).slice(header.inheritedEventCount);
|
|
652
|
+
const fixture = parseSessionFixture(readFileSync(childFile, "utf8"));
|
|
653
|
+
const ownEvents = fixture.events.slice(fixture.inheritedEventCount);
|
|
395
654
|
children.push({
|
|
396
|
-
recordedId:
|
|
397
|
-
createdAt:
|
|
655
|
+
recordedId: fixture.id,
|
|
656
|
+
createdAt: fixture.createdAt,
|
|
398
657
|
entries: deriveReplayScript(ownEvents),
|
|
399
658
|
primary: false
|
|
400
659
|
});
|
|
@@ -659,4 +918,4 @@ function apply(ctx, config = {}) {
|
|
|
659
918
|
});
|
|
660
919
|
}
|
|
661
920
|
//#endregion
|
|
662
|
-
export { apply, deriveReplayScript, inject, installLlmReplay, loadReplayScript, loadSessionScripts, name, parseSessionHeader, parseSessionLog, resolveScriptedEntry };
|
|
921
|
+
export { apply, deriveReplayScript, inject, installLlmReplay, loadReplayScript, loadSessionScripts, name, parseSessionHeader, parseSessionLog, prepareSessionEventNotificationsForComparison, prepareSessionSnapshotFixtureForComparison, resolveScriptedEntry };
|
package/lib/types/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Keyless snapshot-test LLM replay. It derives one model-call script per
|
|
3
|
-
* recorded session from
|
|
3
|
+
* recorded session from v2 embedded Assistant streams and explicitly marked local
|
|
4
4
|
* compaction calls, then binds fresh live sessions to parent/child scripts by
|
|
5
5
|
* first-call order. Throw and hang cases require an explicit override because
|
|
6
6
|
* a session log cannot reconstruct them alone.
|
|
@@ -78,16 +78,17 @@ export interface ReplayProviderConfig {
|
|
|
78
78
|
/** Resolved plugin configuration. */
|
|
79
79
|
export interface ReplayConfig {
|
|
80
80
|
/**
|
|
81
|
-
* Path to the PRIMARY (
|
|
82
|
-
*
|
|
83
|
-
*
|
|
81
|
+
* Path to the selected PRIMARY fixture (`session.jsonl` for v0 or
|
|
82
|
+
* `session.vN.jsonl` for vN). For a single-session scenario this is the only
|
|
83
|
+
* log; for a nested-agent scenario it is the parent, and child logs ride in
|
|
84
|
+
* {@link childFiles}.
|
|
84
85
|
*/
|
|
85
86
|
file: string;
|
|
86
87
|
/**
|
|
87
88
|
* Optional sidecar for the PRIMARY session: a bare `ReplayEntry[]` replaces
|
|
88
89
|
* the derived script; `{ patches }` keeps it and swaps the named call
|
|
89
90
|
* indexes ({@link ReplayOverrideDoc}). Used by single-session scenarios not
|
|
90
|
-
* expressible as
|
|
91
|
+
* expressible as a durable embedded stream (throw-before-chunk, cancel/hang,
|
|
91
92
|
* injected transient failures). Absent for normal and nested scenarios.
|
|
92
93
|
*/
|
|
93
94
|
overrideFile?: string;
|
|
@@ -148,21 +149,34 @@ export interface SessionScript {
|
|
|
148
149
|
primary: boolean;
|
|
149
150
|
}
|
|
150
151
|
/**
|
|
151
|
-
* Parse a session `.jsonl` buffer into
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* header is skipped; malformed lines fail loud.
|
|
152
|
+
* Parse a projected session `.jsonl` buffer into current events. The first
|
|
153
|
+
* non-empty line is the physical header. Body rows either all carry complete
|
|
154
|
+
* persistence envelopes or all omit them; projected rows receive deterministic
|
|
155
|
+
* dense sequences and zero timestamps. The build-static format catalog then
|
|
156
|
+
* decodes and migrates the complete artifact before this function returns.
|
|
157
157
|
* @param text - the raw `.jsonl` file contents.
|
|
158
|
-
* @returns every
|
|
158
|
+
* @returns every migrated current event, in log order.
|
|
159
159
|
*/
|
|
160
160
|
export declare function parseSessionLog(text: string): SessionEvent[];
|
|
161
|
+
/**
|
|
162
|
+
* Convert one persisted or projected snapshot fixture to the current physical format in memory for expected-output comparison.
|
|
163
|
+
* Projected cwd tokens remain tokens so the ordinary snapshot normalizer can compare them with a fresh run.
|
|
164
|
+
* @param text - one complete Session fixture.
|
|
165
|
+
* @returns current-format JSONL with complete event envelopes; the input string and source file remain unchanged.
|
|
166
|
+
*/
|
|
167
|
+
export declare function prepareSessionSnapshotFixtureForComparison(text: string): string;
|
|
168
|
+
/**
|
|
169
|
+
* Project v1 session-event notifications to current settlement cardinality in memory.
|
|
170
|
+
* Non-event protocol rows retain their exact positions, and current v2 input passes through.
|
|
171
|
+
* @param text - headless `session_event` or SDK `session.event` JSONL.
|
|
172
|
+
* @returns comparison JSONL using current Session events without modifying its source file.
|
|
173
|
+
*/
|
|
174
|
+
export declare function prepareSessionEventNotificationsForComparison(text: string): string;
|
|
161
175
|
/**
|
|
162
176
|
* Read replay identity, ordering, and fork-seed facts from the JSONL header.
|
|
163
177
|
*
|
|
164
|
-
* @param text - the raw `.jsonl` file contents
|
|
165
|
-
* @returns the header's `id`, `createdAt`, and inherited-event count
|
|
178
|
+
* @param text - the raw `.jsonl` file contents; the complete artifact is validated and migrated.
|
|
179
|
+
* @returns the migrated header's `id`, `createdAt`, and exact inherited-event count.
|
|
166
180
|
*/
|
|
167
181
|
export declare function parseSessionHeader(text: string): {
|
|
168
182
|
id: string;
|
|
@@ -172,8 +186,7 @@ export declare function parseSessionHeader(text: string): {
|
|
|
172
186
|
/**
|
|
173
187
|
* Reconstruct the per-`stream()` replay script from a recorded session log.
|
|
174
188
|
*
|
|
175
|
-
*
|
|
176
|
-
* to detect an unterminated prior call. A `compaction/summary` explicitly marked
|
|
189
|
+
* Reads one embedded stream from each Assistant settlement. A `compaction/summary` explicitly marked
|
|
177
190
|
* as one local LLM-stream call becomes a canonical successful stream from its
|
|
178
191
|
* complete `rawOutput` at the summary's log position. A
|
|
179
192
|
* missing assistant terminator means the live stream threw, so derivation
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-llm-replay",
|
|
3
3
|
"description": "Replay LLM plugin: short-circuits llm/stream with model chunks reconstructed from a recorded session JSONL (keyless snapshot tests)",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.3-alpha.2",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
"license": "MIT",
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
31
|
-
"@deepseek-ai/dsh-
|
|
32
|
-
"@deepseek-ai/dsh-llm": "^0.1.
|
|
33
|
-
"@deepseek-ai/dsh-
|
|
34
|
-
"@deepseek-ai/dsh-
|
|
31
|
+
"@deepseek-ai/dsh-compaction": "^0.1.3-alpha.2",
|
|
32
|
+
"@deepseek-ai/dsh-deepseek-llm-api-extensions": "^0.1.3-alpha.2",
|
|
33
|
+
"@deepseek-ai/dsh-llm": "^0.1.3-alpha.2",
|
|
34
|
+
"@deepseek-ai/dsh-session": "^0.1.3-alpha.2"
|
|
35
35
|
},
|
|
36
36
|
"peerDependenciesMeta": {
|
|
37
37
|
"@deepseek-ai/dsh-deepseek-llm-api-extensions": {
|
|
@@ -39,13 +39,14 @@
|
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
+
"@deepseek-ai/dsh-compaction": "^0.1.3-alpha.2",
|
|
43
|
+
"@deepseek-ai/dsh-deepseek-llm-api-extensions": "^0.1.3-alpha.2",
|
|
44
|
+
"@deepseek-ai/dsh-llm": "^0.1.3-alpha.2",
|
|
42
45
|
"@deepseek-ai/cordis": "^4.0.2",
|
|
43
|
-
"@deepseek-ai/dsh-
|
|
44
|
-
"@deepseek-ai/dsh-deepseek-llm-api-extensions": "^0.1.2-alpha.5",
|
|
45
|
-
"@deepseek-ai/dsh-llm": "^0.1.2-alpha.5",
|
|
46
|
-
"@deepseek-ai/dsh-session": "^0.1.2-alpha.5"
|
|
46
|
+
"@deepseek-ai/dsh-session": "^0.1.3-alpha.2"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@deepseek-ai/dsh-
|
|
49
|
+
"@deepseek-ai/dsh-session-format-catalog": "^0.1.3-alpha.2",
|
|
50
|
+
"@deepseek-ai/dsh-util-values": "^0.1.3-alpha.2"
|
|
50
51
|
}
|
|
51
52
|
}
|