fix(replay): mark local compaction calls
This commit is contained in:
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/compact/compact-basic/README.md
|
||||
README.md: 0c7b009255dc2d41dc81cf2c7ff745e02ef28b9a
|
||||
README.zh.md: 4af584a059c99725882afd6206bdf9c984c7d4e3
|
||||
README.md: d1c1dfb509ae0750e1237532a829a35de5084c5e
|
||||
README.zh.md: a78887daad04d534ffed9cbd0357b76bdd908f5e
|
||||
|
||||
@@ -21,7 +21,7 @@ This backend owns the compaction policy:
|
||||
- **Overflow recovery** — provider-confirmed overflow needs no capacity metadata: it bypasses normal pressure and retention, prunes, then attempts one maximal balanced head reduction while leaving the newest indivisible unit. Retry is authorized whenever `surface.replaceGeneration` advances, including when pruning lands before later summary work throws. No replacement, an exhausted target-specific cap, cancellation, or an unknown/noncanonical error preserves the original provider failure.
|
||||
- **Failure handling** — a live unmatched `compact/start` is the durable lock. An unmatched marker before a newer `session/end-seed` is stale evidence from a prior lifecycle and does not block; one after that boundary reports `busy`. Summary and changed-span failures close with an error and leave the conversation surface untouched, though the attempt remains in the log. A failed close deliberately leaves a blocking orphan. Operational pressure failures warn and continue, while overflow-recovery failure preserves the original provider error only when no earlier replacement advanced the surface. Cancellation remains authoritative after cleanup and durability.
|
||||
|
||||
The protected `summarize()` method is the sole subclass hook. A template- or remote-summarizer subclass can override it while pressure, retention, provenance, shrink validation, and shadowed-token accounting stay on `ctx.tokenMeter`. The hook returns the safe summary plus the complete provider output, call envelope, and usage when available (`{ summary, rawOutput?, provider, model, maxTokens?, usage? }`); the transaction preserves those fields on `compact/summary`.
|
||||
The protected `summarize()` method is the sole subclass hook. A template- or remote-summarizer subclass can override it while pressure, retention, provenance, shrink validation, and shadowed-token accounting stay on `ctx.tokenMeter`. The hook returns the safe summary plus the complete provider output, call envelope, and usage when available (`{ summary, rawOutput?, llmStreamCall?, provider, model, maxTokens?, usage? }`); `llmStreamCall: true` means producing that result consumed exactly one call through this context's `ctx.llm.stream()`, while `rawOutput` alone does not identify the call path. The transaction preserves those fields on `compact/summary`.
|
||||
|
||||
## Config (`BasicCompactConfig`)
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
- **溢出恢复**:提供方已确认的溢出不需容量元数据。它会绕过常规压力与保留,执行剪枝,再尝试一次最大平衡头部缩减,并留下最新不可分单元。只要 `surface.replaceGeneration` 前进,就允许重试,包括剪枝在后续摘要工作抛出异常前已落地的情况。如果没有替换、目标特定上限已耗尽、已取消,或遇到未知/非规范错误,则保留原始提供方失败。
|
||||
- **失败处理**:活动的未匹配 `compact/start` 是持久锁。位于较新 `session/end-seed` 之前的未匹配标记,是先前生命周期留下的陈旧证据,不会阻塞;位于该边界之后的标记报告 `busy`。摘要和 span 变更失败会以错误闭合,并保持会话表层不变,但日志中仍保留该尝试。闭合失败会有意留下阻塞性的未匹配标记。压力检查中的运行故障会发出警告并继续;只有此前没有替换推进表层时,溢出恢复失败才保留原始提供方错误。完成清理与持久化后,取消仍具有最终决定权。
|
||||
|
||||
受保护的 `summarize()` 方法是唯一的子类钩子。基于模板或远程摘要器的子类可以覆盖该方法,同时压力、保留、溯源、缩减验证与已遮蔽 token 计量仍由 `ctx.tokenMeter` 负责。钩子返回安全摘要,以及完整提供方输出、调用 envelope 和可用时的 usage(`{ summary, rawOutput?, provider, model, maxTokens?, usage? }`);事务会在 `compact/summary` 上保留这些字段。
|
||||
受保护的 `summarize()` 方法是唯一的子类钩子。基于模板或远程摘要器的子类可以覆盖该方法,同时压力、保留、溯源、缩减验证与已遮蔽 token 计量仍由 `ctx.tokenMeter` 负责。钩子返回安全摘要,以及完整提供方输出、调用 envelope 和可用时的 usage(`{ summary, rawOutput?, llmStreamCall?, provider, model, maxTokens?, usage? }`);`llmStreamCall: true` 表示生成该结果时恰好通过此上下文的 `ctx.llm.stream()` 发起了一次调用,而单有 `rawOutput` 并不能判定调用路径。事务会在 `compact/summary` 上保留这些字段。
|
||||
|
||||
## 配置(`BasicCompactConfig`)
|
||||
|
||||
|
||||
@@ -416,6 +416,7 @@ function commitCompactionBody(
|
||||
shadowedTokenCount,
|
||||
summary,
|
||||
rawOutput,
|
||||
llmStreamCall,
|
||||
provider,
|
||||
model,
|
||||
maxTokens,
|
||||
@@ -425,6 +426,7 @@ function commitCompactionBody(
|
||||
const summaryEvent = session.append('compact/summary', {
|
||||
summary,
|
||||
...rawOutput === undefined ? {} : { rawOutput },
|
||||
...llmStreamCall === undefined ? {} : { llmStreamCall },
|
||||
shadowedRange: { start, end },
|
||||
shadowedSeqs: [...shadowedSeqs],
|
||||
shadowedTokenCount,
|
||||
|
||||
@@ -87,8 +87,16 @@ export interface SummarizationInput {
|
||||
/** Safe summary content plus the exact auxiliary call envelope recorded in provenance. */
|
||||
export interface SummaryResult {
|
||||
summary: ContentBlock[]
|
||||
/** Complete provider output before the text-only summary projection. */
|
||||
/**
|
||||
* Complete provider output before the text-only summary projection; this
|
||||
* alone does not identify the call path.
|
||||
*/
|
||||
rawOutput?: ContentBlock[]
|
||||
/**
|
||||
* Present only when producing the summary consumed exactly one call through
|
||||
* this context's `ctx.llm.stream()`.
|
||||
*/
|
||||
llmStreamCall?: true
|
||||
provider: string
|
||||
model: string
|
||||
maxTokens?: number
|
||||
@@ -162,6 +170,7 @@ export async function summarizeWithLlm(
|
||||
return {
|
||||
summary,
|
||||
rawOutput,
|
||||
llmStreamCall: true,
|
||||
provider: options.provider,
|
||||
model: options.model,
|
||||
maxTokens: config.maxTokens,
|
||||
|
||||
@@ -868,6 +868,7 @@ describe('compaction region transaction', () => {
|
||||
rawOutput: compact.rawOutput,
|
||||
usage: compact.usage,
|
||||
})
|
||||
expect(summary?.data).not.toHaveProperty('llmStreamCall')
|
||||
const head = session.deriveMessages()[0]!
|
||||
expect(head.content[0]?.type).toBe('text')
|
||||
expect(head.content[0]?.type === 'text' ? head.content[0].text : '').toContain('<compacted-summary>')
|
||||
@@ -1187,6 +1188,7 @@ describe('default one-shot summarizer', () => {
|
||||
{ type: 'text', text: 'public summary' },
|
||||
{ type: 'tool-call', id: CallId('unexpected'), name: 'x', arguments: '{}' },
|
||||
],
|
||||
llmStreamCall: true,
|
||||
provider: MODEL,
|
||||
model: MODEL,
|
||||
maxTokens: 321,
|
||||
@@ -1300,6 +1302,7 @@ describe('default one-shot summarizer', () => {
|
||||
await compact.compactRegion(nodes[0]!, nodes[3]!, agent(session, MODEL), SIGNAL)
|
||||
expect(session.events.findLast(event => event.type === 'compact/summary')?.data).toMatchObject({
|
||||
summary: [{ type: 'text', text: 'routed summary' }],
|
||||
llmStreamCall: true,
|
||||
provider: 'routed-summary-provider',
|
||||
model: 'routed-summary-model',
|
||||
})
|
||||
|
||||
@@ -28,8 +28,16 @@ declare module '@deepseek-ai/dsh-session' {
|
||||
*/
|
||||
'compact/summary': {
|
||||
summary: ContentBlock[]
|
||||
/** Complete provider output before the backend's safe summary projection. */
|
||||
/**
|
||||
* Complete provider output before the backend's safe summary projection;
|
||||
* this alone does not identify the call path.
|
||||
*/
|
||||
rawOutput?: ContentBlock[]
|
||||
/**
|
||||
* Present only when producing the summary consumed exactly one call
|
||||
* through this context's `ctx.llm.stream()`.
|
||||
*/
|
||||
llmStreamCall?: true
|
||||
shadowedRange: { start: number; end: number }
|
||||
shadowedSeqs: number[]
|
||||
shadowedTokenCount: number
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
||||
# after editing either side, bring the other along and re-record with:
|
||||
# pnpm run verify-translation-pairing --write packages/support/llm-replay/README.md
|
||||
README.md: 46d391970f320708914d11f0868cbbc5361ae196
|
||||
README.zh.md: a67b078a1396968dc3ddecb0e616a832c4faaf3a
|
||||
README.md: a6c087778b8e64124590fb6be7a652b58e1b6343
|
||||
README.zh.md: 241edb9c9b2400519155ccd7161eb7f34b9f5bbe
|
||||
|
||||
@@ -8,7 +8,7 @@ Its consumers are the ACP and headless `stream-json` snapshot suites plus the We
|
||||
|
||||
## How the fixture works
|
||||
|
||||
The fixture IS the persisted session log (`<scenario>/session.jsonl`). Its `assistant/chunk` events carry every `StreamChunk`, so grouping them by `(turn, step)` reconstructs each agent-loop `stream()` call's chunk sequence. A successful compaction summarizer is logged differently: when `compact/summary` carries its complete `rawOutput`, replay reconstructs a canonical successful stream at that event's position using one `block-start`/`block-end` pair per block, the recorded usage when present, and a terminal `stop`. Exact provider delta partitioning is not part of the durable compaction result. A summary without `rawOutput` does not imply an LLM call because template and remote summarizers may produce it without the local adapter.
|
||||
The fixture IS the persisted session log (`<scenario>/session.jsonl`). Its `assistant/chunk` events carry every `StreamChunk`, so grouping them by `(turn, step)` reconstructs each agent-loop `stream()` call's chunk sequence. A successful compaction summarizer is logged differently: when `compact/summary` carries `llmStreamCall: true` and its complete `rawOutput`, replay reconstructs a canonical successful stream at that event's position using one `block-start`/`block-end` pair per block, the recorded usage when present, and a terminal `stop`. Exact provider delta partitioning is not part of the durable compaction result. `rawOutput` without the marker does not imply a local LLM call because template and remote summarizers may retain complete output without using this context's adapter.
|
||||
|
||||
Recording is therefore "run the real agent once and harvest the `.jsonl`", done by the snapshot harness — this plugin does not record. A fixture may carry its `request/header` content tokenized to `{{system}}`/`{{tools}}` (the harness pins that content in one scenario and scrubs the rest); replay is indifferent — derivation reads only `assistant/chunk` and `compact/summary` events plus the line-0 session header.
|
||||
|
||||
@@ -59,7 +59,7 @@ Replay keys every call by its calling session id (`GenerateOptions.sessionId`, s
|
||||
- `installLlmReplay(ctx, config)` — install the configured replay adapter or catch-all `llm/stream` listener; returns a `ReplayHandle` (`dispose()` for HMR safety plus `assertConsumed()`, the teardown check that every recorded script bound to a live session and every bound cursor drained — turning a scenario that silently drove fewer model calls than recorded into a crisp diagnostic). Use this in tests to drive replay without the Loader or env vars.
|
||||
- `loadSessionScripts(config)` — resolve the ordered `SessionScript[]` (primary + children) for a scenario, ready to bind to live sessions in first-call order.
|
||||
- `loadReplayScript(config)` — resolve the `ReplayEntry[]` for the primary session only (validated sidecar replacement/patches if present, else derived from the JSONL; fail-loud if the fixture is missing).
|
||||
- `deriveReplayScript(events)` / `parseSessionLog(text)` / `parseSessionHeader(text)` / `resolveScriptedEntry(entry, messages)` — the pure helpers that turn ordinary loop chunks and complete compaction outputs in a recorded session log into a script, read its header `id`/`createdAt`, and resolve `{{fromRequest:...}}` placeholders against one live request. A derived assistant group must end in a `finish` chunk; a group without one is the fingerprint of a thrown `stream()` and must instead be expressed via an override sidecar.
|
||||
- `deriveReplayScript(events)` / `parseSessionLog(text)` / `parseSessionHeader(text)` / `resolveScriptedEntry(entry, messages)` — the pure helpers that turn ordinary loop chunks and explicitly marked local compaction outputs in a recorded session log into a script, read its header `id`/`createdAt`, and resolve `{{fromRequest:...}}` placeholders against one live request. A derived assistant group must end in a `finish` chunk; a group without one is the fingerprint of a thrown `stream()` and must instead be expressed via an override sidecar.
|
||||
- Types `ReplayEntry` / `ReplayOverrideDoc` / `ReplayOverridePatch` / `SessionScript` / `ReplayConfig` / `ReplayProviderConfig` / `ReplayModelConfig` / `ReplayHandle` / `Config`.
|
||||
|
||||
## Plugin export shape
|
||||
@@ -77,4 +77,4 @@ None; this package neither assembles nor sends a provider request.
|
||||
## Known Limitations and Deferred Work
|
||||
|
||||
- **First-call-order script binding assumes sequential delegation** — a cut that runs sibling subagents concurrently would bind live sessions to recorded scripts non-deterministically; a stronger keying is deferred until such a scenario exists (`XXX(concurrent-subagents)`).
|
||||
- **Only ordinary loop chunks and completed compaction outputs are derivable** — a pure pre-chunk throw or a cancel/hang scenario needs the `replay.override.json` sidecar. Replacement and patch forms affect only the primary session; child scripts still derive from their logs.
|
||||
- **Only ordinary loop chunks and marked local compaction outputs are derivable** — a pure pre-chunk throw, a cancel/hang, or an unmarked external summarizer call needs the `replay.override.json` sidecar. Replacement and patch forms affect only the primary session; child scripts still derive from their logs.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
## fixture 的工作方式
|
||||
|
||||
fixture 就是持久化的会话日志(`<scenario>/session.jsonl`)。其 `assistant/chunk` 事件包含每个 `StreamChunk`,因此按 `(turn, step)` 分组即可重建每次 agent-loop `stream()` 调用的分片序列。压缩(compaction)摘要器成功时,日志记录方式有所不同:当 `compact/summary` 携带完整的 `rawOutput` 时,回放会在该事件的位置重建一条规范成功流,其中每个块各使用一对 `block-start`/`block-end`,带上已记录的 usage(如有),并以 `stop` 终止。提供方增量的精确切分不属于持久压缩结果。不带 `rawOutput` 的摘要并不意味着发生了 LLM 调用,因为模板摘要器和远程摘要器可能不经本地适配器生成该摘要。
|
||||
fixture 就是持久化的会话日志(`<scenario>/session.jsonl`)。其 `assistant/chunk` 事件包含每个 `StreamChunk`,因此按 `(turn, step)` 分组即可重建每次 agent-loop `stream()` 调用的分片序列。压缩(compaction)摘要器成功时,日志记录方式有所不同:当 `compact/summary` 携带 `llmStreamCall: true` 和完整的 `rawOutput` 时,回放会在该事件的位置重建一条规范成功流,其中每个块各使用一对 `block-start`/`block-end`,带上已记录的 usage(如有),并以 `stop` 终止。提供方增量的精确切分不属于持久压缩结果。不带该标记的 `rawOutput` 并不意味着发生了本地 LLM 调用,因为模板摘要器和远程摘要器即使未使用此上下文的适配器,也可能保留完整输出。
|
||||
|
||||
因此,录制就是「运行一次真实 agent 并收集 `.jsonl`」,由快照 harness 完成;该插件本身不录制。fixture 的 `request/header` 内容可能被标记化为 `{{system}}`/`{{tools}}`(harness 会在一个场景中固定该内容,并清除其余场景中的内容);回放不受影响,因为派生过程只读取 `assistant/chunk` 和 `compact/summary` 事件以及第 0 行的会话 header。
|
||||
|
||||
@@ -59,7 +59,7 @@ fixture 就是持久化的会话日志(`<scenario>/session.jsonl`)。其 `as
|
||||
- `installLlmReplay(ctx, config)`:安装已配置回放适配器或 catch-all `llm/stream` 监听器;返回 `ReplayHandle`(包含用于保证 HMR(热模块替换)安全的 `dispose()`,以及清理阶段执行的 `assertConsumed()` 检查;后者确保每个已记录脚本都绑定到实时会话,且每个已绑定游标都已耗尽,从而将场景静默驱动的模型调用少于记录数转换为明确诊断)。在测试中使用它,可以不通过 Loader 或 env var 驱动回放。
|
||||
- `loadSessionScripts(config)`:解析场景中有序的 `SessionScript[]`(主会话 + 子会话),准备按首次调用顺序绑定到实时会话。
|
||||
- `loadReplayScript(config)`:只解析主会话的 `ReplayEntry[]`(如果伴随文件存在,则使用经校验的替换或补丁;否则从 JSONL 派生;fixture 缺失时明确报错)。
|
||||
- `deriveReplayScript(events)` / `parseSessionLog(text)` / `parseSessionHeader(text)` / `resolveScriptedEntry(entry, messages)`:将已记录会话日志中的普通 loop 分片和完整压缩输出转换为脚本、读取其 header `id`/`createdAt`、并针对单次实时请求解析 `{{fromRequest:...}}` 占位符的纯辅助工具。派生的 assistant 分组必须以 `finish` 分片结束;没有该分片的分组是 `stream()` 抛出异常的指纹,必须改用 override 伴随文件表达。
|
||||
- `deriveReplayScript(events)` / `parseSessionLog(text)` / `parseSessionHeader(text)` / `resolveScriptedEntry(entry, messages)`:将已记录会话日志中的普通 loop 分片和显式标记的本地压缩输出转换为脚本、读取其 header `id`/`createdAt`、并针对单次实时请求解析 `{{fromRequest:...}}` 占位符的纯辅助工具。派生的 assistant 分组必须以 `finish` 分片结束;没有该分片的分组是 `stream()` 抛出异常的指纹,必须改用 override 伴随文件表达。
|
||||
- 类型 `ReplayEntry` / `ReplayOverrideDoc` / `ReplayOverridePatch` / `SessionScript` / `ReplayConfig` / `ReplayProviderConfig` / `ReplayModelConfig` / `ReplayHandle` / `Config`。
|
||||
|
||||
## 插件导出形态
|
||||
@@ -77,4 +77,4 @@ fixture 就是持久化的会话日志(`<scenario>/session.jsonl`)。其 `as
|
||||
## 已知限制与暂缓事项
|
||||
|
||||
- **首次调用顺序脚本绑定假设串行委托**:并发运行同级 subagent 的 cut 会非确定性地将实时会话绑定到已记录脚本;在这种场景出现前暂不实现更强的键控(`XXX(concurrent-subagents)`)。
|
||||
- **只有普通 loop 分片和已完成的压缩输出才能派生**:在产生分片前直接抛出异常或取消/挂起的场景需要 `replay.override.json` 伴随文件。替换和补丁两种形式都只影响主会话;子会话脚本仍从各自日志派生。
|
||||
- **只有普通 loop 分片和带标记的本地压缩输出才能派生**:在产生分片前直接抛出异常、取消/挂起,或未标记的外部摘要器调用场景需要 `replay.override.json` 伴随文件。替换和补丁两种形式都只影响主会话;子会话脚本仍从各自日志派生。
|
||||
|
||||
@@ -177,8 +177,9 @@ export function parseSessionHeader(text: string): { id: string; createdAt: numbe
|
||||
* Reconstruct the per-`stream()` replay script from a recorded session log.
|
||||
*
|
||||
* Splits `assistant/chunk` events at every `finish`, using turn and step changes
|
||||
* to detect an unterminated prior call. A complete `compact/summary.rawOutput`
|
||||
* becomes a canonical successful stream at the summary's log position. A
|
||||
* to detect an unterminated prior call. A `compact/summary` explicitly marked
|
||||
* as one local LLM-stream call becomes a canonical successful stream from its
|
||||
* complete `rawOutput` at the summary's log position. A
|
||||
* missing assistant terminator means the live stream threw, so derivation
|
||||
* rejects and the scenario must provide an explicit override. Multiple calls
|
||||
* may share one turn and step when the loop retries.
|
||||
@@ -204,7 +205,10 @@ export function deriveReplayScript(events: SessionEvent[]): ReplayEntry[] {
|
||||
close(currentKey, current)
|
||||
currentKey = undefined
|
||||
current = []
|
||||
if (event.data.rawOutput !== undefined) {
|
||||
if (event.data.llmStreamCall === true) {
|
||||
if (event.data.rawOutput === undefined) {
|
||||
throw new Error('llm-replay: compact/summary marks an LLM stream call without rawOutput')
|
||||
}
|
||||
const chunks: StreamChunk[] = []
|
||||
for (const [index, block] of event.data.rawOutput.entries()) {
|
||||
chunks.push({ type: 'block-start', index, blockType: block.type })
|
||||
|
||||
@@ -202,6 +202,7 @@ describe('deriveReplayScript', () => {
|
||||
data: {
|
||||
summary: rawOutput,
|
||||
rawOutput,
|
||||
llmStreamCall: true,
|
||||
shadowedRange: { start: 1, end: 1 },
|
||||
shadowedSeqs: [1],
|
||||
shadowedTokenCount: 20,
|
||||
@@ -238,6 +239,45 @@ describe('deriveReplayScript', () => {
|
||||
expect(deriveReplayScript([event])).toEqual([])
|
||||
})
|
||||
|
||||
it('does not infer a local LLM call from external compact output', () => {
|
||||
const block = { type: 'text' as const, text: 'remote summary' }
|
||||
const event: SessionEvent<'compact/summary'> = {
|
||||
type: 'compact/summary',
|
||||
seq: 1,
|
||||
time: 0,
|
||||
data: {
|
||||
summary: [block],
|
||||
rawOutput: [block],
|
||||
shadowedRange: { start: 1, end: 1 },
|
||||
shadowedSeqs: [1],
|
||||
shadowedTokenCount: 20,
|
||||
provider: 'remote',
|
||||
model: 'remote',
|
||||
},
|
||||
}
|
||||
|
||||
expect(deriveReplayScript([event])).toEqual([])
|
||||
})
|
||||
|
||||
it('rejects a marked compact LLM call without its complete output', () => {
|
||||
const event: SessionEvent<'compact/summary'> = {
|
||||
type: 'compact/summary',
|
||||
seq: 1,
|
||||
time: 0,
|
||||
data: {
|
||||
summary: [{ type: 'text', text: 'incomplete provenance' }],
|
||||
llmStreamCall: true,
|
||||
shadowedRange: { start: 1, end: 1 },
|
||||
shadowedSeqs: [1],
|
||||
shadowedTokenCount: 20,
|
||||
provider: 'mock',
|
||||
model: 'mock',
|
||||
},
|
||||
}
|
||||
|
||||
expect(() => deriveReplayScript([event])).toThrow(/LLM stream call without rawOutput/)
|
||||
})
|
||||
|
||||
it('derives a compact/summary stream when usage is unavailable', () => {
|
||||
const block = { type: 'text' as const, text: 'summary without usage' }
|
||||
const event: SessionEvent<'compact/summary'> = {
|
||||
@@ -247,6 +287,7 @@ describe('deriveReplayScript', () => {
|
||||
data: {
|
||||
summary: [block],
|
||||
rawOutput: [block],
|
||||
llmStreamCall: true,
|
||||
shadowedRange: { start: 1, end: 1 },
|
||||
shadowedSeqs: [1],
|
||||
shadowedTokenCount: 20,
|
||||
@@ -289,6 +330,27 @@ describe('deriveReplayScript', () => {
|
||||
]
|
||||
expect(() => deriveReplayScript(events)).toThrow(/model call 1\/1 ended without a finish chunk/)
|
||||
})
|
||||
|
||||
it('rejects an unfinished call at a compact summary boundary', () => {
|
||||
const events: SessionEvent[] = [
|
||||
chunkEvent(1, 1, 1, { type: 'block-start', index: 0, blockType: 'text' }),
|
||||
{
|
||||
type: 'compact/summary',
|
||||
seq: 2,
|
||||
time: 0,
|
||||
data: {
|
||||
summary: [{ type: 'text', text: 'external checkpoint' }],
|
||||
shadowedRange: { start: 1, end: 1 },
|
||||
shadowedSeqs: [1],
|
||||
shadowedTokenCount: 20,
|
||||
provider: 'external',
|
||||
model: 'external',
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
expect(() => deriveReplayScript(events)).toThrow(/model call 1\/1 ended without a finish chunk/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('loadReplayScript', () => {
|
||||
|
||||
Reference in New Issue
Block a user