diff --git a/scripts/source-file-size-baseline.json b/scripts/source-file-size-baseline.json index 3415a93df1..21db113be1 100644 --- a/scripts/source-file-size-baseline.json +++ b/scripts/source-file-size-baseline.json @@ -17,7 +17,7 @@ "src/cli/components/ProviderSelector.tsx": 1692, "src/cli/components/ToolCallMessageRich.tsx": 1109, "src/cli/helpers/accumulator.ts": 1600, - "src/cli/helpers/reflection-transcript.ts": 1956, + "src/cli/helpers/reflection-transcript.ts": 1816, "src/cli/mods/local-mod-loader.test.ts": 1043, "src/cli/reflection-transcript.test.ts": 1084, "src/cli/subcommands/skills.ts": 1264, diff --git a/src/cli/helpers/reflection-step-state.ts b/src/cli/helpers/reflection-step-state.ts new file mode 100644 index 0000000000..a3d93e1628 --- /dev/null +++ b/src/cli/helpers/reflection-step-state.ts @@ -0,0 +1,152 @@ +export const REFLECTION_STATE_SCHEMA_VERSION = + "v4_canonical_assistant_steps" as const; + +const LEGACY_STATE_SCHEMA_VERSIONS = new Set([ + "v2_message_id", + "v3_assistant_steps", +]); + +export interface ReflectionTranscriptState { + schema_version: typeof REFLECTION_STATE_SCHEMA_VERSION; + reflected_through_message_id?: string; + total_completed_steps: number; + reflected_completed_steps: number; + steps_since_last_successful_reflection: number; + last_reflection_started_at?: string; + last_reflection_succeeded_at?: string; +} + +export interface ReflectionStepEntry { + kind: string; + source_message_id?: string; +} + +type StoredReflectionState = Record; + +function nonEmptyString(value: unknown): string | undefined { + return typeof value === "string" && value.trim().length > 0 + ? value + : undefined; +} + +function nonNegativeInteger(value: unknown): number { + return typeof value === "number" && + Number.isFinite(value) && + Number.isInteger(value) && + value >= 0 + ? value + : 0; +} + +export function normalizeCurrentReflectionTranscriptState( + parsed: StoredReflectionState | null, +): ReflectionTranscriptState | null { + if (parsed?.schema_version !== REFLECTION_STATE_SCHEMA_VERSION) { + return null; + } + const totalCompletedSteps = nonNegativeInteger(parsed.total_completed_steps); + const reflectedCompletedSteps = Math.min( + nonNegativeInteger(parsed.reflected_completed_steps), + totalCompletedSteps, + ); + return { + schema_version: REFLECTION_STATE_SCHEMA_VERSION, + reflected_through_message_id: nonEmptyString( + parsed.reflected_through_message_id, + ), + total_completed_steps: totalCompletedSteps, + reflected_completed_steps: reflectedCompletedSteps, + steps_since_last_successful_reflection: Math.max( + 0, + totalCompletedSteps - reflectedCompletedSteps, + ), + last_reflection_started_at: nonEmptyString( + parsed.last_reflection_started_at, + ), + last_reflection_succeeded_at: nonEmptyString( + parsed.last_reflection_succeeded_at, + ), + }; +} + +export function countCanonicalAssistantSteps( + entries: ReflectionStepEntry[], +): number { + const canonicalIds = new Set(); + let fallbackRows = 0; + for (const entry of entries) { + if (entry.kind !== "assistant") continue; + const messageId = nonEmptyString(entry.source_message_id); + if (messageId) canonicalIds.add(messageId); + else fallbackRows += 1; + } + return canonicalIds.size + fallbackRows; +} + +function isCanonicalAnchorEntry(entry: ReflectionStepEntry): boolean { + return ( + (entry.kind === "user" || entry.kind === "assistant") && + nonEmptyString(entry.source_message_id) !== undefined + ); +} + +function lastAnchorIndex( + entries: ReflectionStepEntry[], + reflectedThroughMessageId?: string, +): number { + if (!reflectedThroughMessageId) return -1; + for (let index = entries.length - 1; index >= 0; index -= 1) { + const entry = entries[index]; + if ( + entry && + isCanonicalAnchorEntry(entry) && + entry.source_message_id === reflectedThroughMessageId + ) { + return index; + } + } + return -1; +} + +/** + * Rebuild counters from the transcript during state migration. Older schemas + * counted display rows, so their numeric counters are not reliable. The + * canonical message anchor is reliable when it still exists; otherwise replay + * all transcript steps rather than marking unseen content complete. + */ +export function rebuildReflectionTranscriptState( + parsed: StoredReflectionState | null, + entries: ReflectionStepEntry[], +): ReflectionTranscriptState { + const schemaVersion = parsed?.schema_version; + const canMigrateMetadata = + typeof schemaVersion === "string" && + LEGACY_STATE_SCHEMA_VERSIONS.has(schemaVersion); + const reflectedThroughMessageId = canMigrateMetadata + ? nonEmptyString(parsed?.reflected_through_message_id) + : undefined; + const anchorIndex = lastAnchorIndex(entries, reflectedThroughMessageId); + const totalCompletedSteps = countCanonicalAssistantSteps(entries); + const reflectedCompletedSteps = + anchorIndex < 0 + ? 0 + : countCanonicalAssistantSteps(entries.slice(0, anchorIndex + 1)); + + return { + schema_version: REFLECTION_STATE_SCHEMA_VERSION, + reflected_through_message_id: + anchorIndex < 0 ? undefined : reflectedThroughMessageId, + total_completed_steps: totalCompletedSteps, + reflected_completed_steps: reflectedCompletedSteps, + steps_since_last_successful_reflection: Math.max( + 0, + totalCompletedSteps - reflectedCompletedSteps, + ), + last_reflection_started_at: canMigrateMetadata + ? nonEmptyString(parsed?.last_reflection_started_at) + : undefined, + last_reflection_succeeded_at: canMigrateMetadata + ? nonEmptyString(parsed?.last_reflection_succeeded_at) + : undefined, + }; +} diff --git a/src/cli/helpers/reflection-transcript.ts b/src/cli/helpers/reflection-transcript.ts index a911dcc84e..6310f3deae 100644 --- a/src/cli/helpers/reflection-transcript.ts +++ b/src/cli/helpers/reflection-transcript.ts @@ -21,34 +21,18 @@ import { withFileLock } from "@/utils/file-lock"; import { parseFrontmatter } from "@/utils/frontmatter"; import { getTranscriptRoot } from "@/utils/transcript-paths"; import type { Line } from "./accumulator"; +import { + countCanonicalAssistantSteps, + normalizeCurrentReflectionTranscriptState, + type ReflectionTranscriptState, + rebuildReflectionTranscriptState, +} from "./reflection-step-state"; import { safeJsonParseOr } from "./safe-json-parse"; -const LEGACY_MESSAGE_ID_STATE_SCHEMA_VERSION = "v2_message_id"; -export const REFLECTION_STATE_SCHEMA_VERSION = "v3_assistant_steps" as const; - -export interface ReflectionTranscriptState { - schema_version: typeof REFLECTION_STATE_SCHEMA_VERSION; - reflected_through_message_id?: string; - total_completed_steps: number; - reflected_completed_steps: number; - steps_since_last_successful_reflection: number; - last_reflection_started_at?: string; - last_reflection_succeeded_at?: string; -} - -interface LegacyMessageIdReflectionTranscriptState { - schema_version: typeof LEGACY_MESSAGE_ID_STATE_SCHEMA_VERSION; - reflected_through_message_id?: string; - total_completed_turns?: number; - reflected_completed_turns?: number; - turns_since_last_successful_reflection?: number; - last_reflection_started_at?: string; - last_reflection_succeeded_at?: string; -} - -type StoredReflectionTranscriptState = - | Partial - | Partial; +export { + REFLECTION_STATE_SCHEMA_VERSION, + type ReflectionTranscriptState, +} from "./reflection-step-state"; type TranscriptEntry = | { @@ -643,10 +627,6 @@ function isEligibleCanonicalEntry( ); } -function countAssistantRows(entries: TranscriptEntry[]): number { - return entries.filter((entry) => entry.kind === "assistant").length; -} - /** Maximum characters to keep for tool-call arguments in the reflection payload. */ const TOOL_ARGS_TRUNCATE_LIMIT = 300; @@ -756,117 +736,6 @@ function normalizeString(value: unknown): string | undefined { return typeof value === "string" && value.length > 0 ? value : undefined; } -function normalizeNonNegativeInteger(value: unknown, fallback = 0): number { - return typeof value === "number" && - Number.isFinite(value) && - Number.isInteger(value) && - value >= 0 - ? value - : fallback; -} - -function normalizeV3State( - parsed: Partial, -): ReflectionTranscriptState { - const totalCompletedSteps = normalizeNonNegativeInteger( - parsed.total_completed_steps, - ); - const reflectedCompletedSteps = Math.min( - normalizeNonNegativeInteger(parsed.reflected_completed_steps), - totalCompletedSteps, - ); - const stepsSinceLastSuccessfulReflection = Math.max( - 0, - totalCompletedSteps - reflectedCompletedSteps, - ); - - return { - schema_version: REFLECTION_STATE_SCHEMA_VERSION, - reflected_through_message_id: normalizeString( - parsed.reflected_through_message_id, - ), - total_completed_steps: totalCompletedSteps, - reflected_completed_steps: reflectedCompletedSteps, - steps_since_last_successful_reflection: stepsSinceLastSuccessfulReflection, - last_reflection_started_at: normalizeString( - parsed.last_reflection_started_at, - ), - last_reflection_succeeded_at: normalizeString( - parsed.last_reflection_succeeded_at, - ), - }; -} - -function countAssistantRowsThroughMessageId( - rows: ParsedTranscriptRow[], - reflectedThroughMessageId?: string, -): number { - if (!reflectedThroughMessageId) { - return 0; - } - const anchorRow = rows.find( - (row) => - isEligibleCanonicalEntry(row.entry) && - row.entry.source_message_id === reflectedThroughMessageId, - ); - if (!anchorRow) { - return 0; - } - return countAssistantRows( - rows - .filter((row) => row.lineIndex <= anchorRow.lineIndex) - .map((row) => row.entry), - ); -} - -function migrateMessageIdState( - parsed: Partial, - lines: string[], -): ReflectionTranscriptState { - const rows = parseTranscriptRows(lines); - const allEntries = rows.map((row) => row.entry); - const totalCompletedSteps = countAssistantRows(allEntries); - const reflectedThroughMessageId = normalizeString( - parsed.reflected_through_message_id, - ); - const reflectedCompletedSteps = Math.min( - countAssistantRowsThroughMessageId(rows, reflectedThroughMessageId), - totalCompletedSteps, - ); - - return { - schema_version: REFLECTION_STATE_SCHEMA_VERSION, - reflected_through_message_id: reflectedThroughMessageId, - total_completed_steps: totalCompletedSteps, - reflected_completed_steps: reflectedCompletedSteps, - steps_since_last_successful_reflection: Math.max( - 0, - totalCompletedSteps - reflectedCompletedSteps, - ), - last_reflection_started_at: normalizeString( - parsed.last_reflection_started_at, - ), - last_reflection_succeeded_at: normalizeString( - parsed.last_reflection_succeeded_at, - ), - }; -} - -function buildUnreflectedStateFromTranscript( - lines: string[], -): ReflectionTranscriptState { - const rows = parseTranscriptRows(lines); - const allEntries = rows.map((row) => row.entry); - const totalCompletedSteps = countAssistantRows(allEntries); - - return { - schema_version: REFLECTION_STATE_SCHEMA_VERSION, - total_completed_steps: totalCompletedSteps, - reflected_completed_steps: 0, - steps_since_last_successful_reflection: totalCompletedSteps, - }; -} - async function readState( paths: ReflectionTranscriptPaths, ): Promise { @@ -877,36 +746,20 @@ async function readState( raw = null; } const parsed = raw - ? safeJsonParseOr(raw, null) + ? safeJsonParseOr | null>(raw, null) : null; - const schemaVersion = - parsed && "schema_version" in parsed ? parsed.schema_version : undefined; - - if (schemaVersion === REFLECTION_STATE_SCHEMA_VERSION) { - const state = normalizeV3State( - parsed as Partial, - ); - if (JSON.stringify(state) !== JSON.stringify(parsed)) { - await writeState(paths, state); + const current = normalizeCurrentReflectionTranscriptState(parsed); + if (current) { + if (JSON.stringify(current) !== JSON.stringify(parsed)) { + await writeState(paths, current); } - return state; + return current; } - const transcriptLines = await readTranscriptLines(paths); - - if (!parsed) { - const state = buildUnreflectedStateFromTranscript(transcriptLines); - await writeState(paths, state); - return state; - } - - const migrated = - schemaVersion === LEGACY_MESSAGE_ID_STATE_SCHEMA_VERSION - ? migrateMessageIdState( - parsed as Partial, - transcriptLines, - ) - : buildUnreflectedStateFromTranscript(transcriptLines); + const entries = parseTranscriptRows(await readTranscriptLines(paths)).map( + (row) => row.entry, + ); + const migrated = rebuildReflectionTranscriptState(parsed, entries); await writeState(paths, migrated); return migrated; } @@ -954,6 +807,13 @@ export function getReflectionTranscriptPaths( }; } +/** + * Append one completed successful turn. Canonical assistant ids may repeat + * within this delta when token streaming splits one message into display rows, + * but real-time producers never spread one backend assistant message across + * separate calls. Keep this O(delta); external ingestion has its own cross-call + * source-id filter below. + */ export async function appendTranscriptDeltaJsonl( agentId: string, conversationId: string, @@ -974,7 +834,7 @@ export async function appendTranscriptDeltaJsonl( const payload = entries.map((entry) => JSON.stringify(entry)).join("\n"); await appendFile(paths.transcriptPath, `${payload}\n`, "utf-8"); - state.total_completed_steps += countAssistantRows(entries); + state.total_completed_steps += countCanonicalAssistantSteps(entries); await writeState(paths, state); return entries.length; }); @@ -1076,7 +936,7 @@ export async function appendExternalTranscriptEntries( const payload = fresh.map((entry) => JSON.stringify(entry)).join("\n"); await appendFile(paths.transcriptPath, `${payload}\n`, "utf-8"); - state.total_completed_steps += countAssistantRows(fresh); + state.total_completed_steps += countCanonicalAssistantSteps(fresh); await writeState(paths, state); return { appended: fresh.length, skipped }; }); @@ -1100,7 +960,7 @@ function selectUnreflectedTranscriptRange( const anchorRow = reflectedThroughMessageId === undefined ? undefined - : rows.find( + : rows.findLast( (row) => isEligibleCanonicalEntry(row.entry) && row.entry.source_message_id === reflectedThroughMessageId, @@ -1852,7 +1712,7 @@ export async function buildMultiReflectionPayload( start_line: selection.startLineIndex, end_line: selection.endLineIndex, end_snapshot_line: selection.endLineIndex + 1, - completed_turns: countAssistantRows(entries), + completed_turns: countCanonicalAssistantSteps(entries), approx_chars: approxChars, last_updated_at: await getTranscriptLastUpdatedAt(paths), } satisfies MultiReflectionTranscriptSlice; @@ -1923,7 +1783,7 @@ export async function finalizeAutoReflectionPayload( } const nowIso = new Date().toISOString(); state.reflected_through_message_id = selection.endMessageId; - state.reflected_completed_steps = countAssistantRows( + state.reflected_completed_steps = countCanonicalAssistantSteps( snapshotRows.map((row) => row.entry), ); state.last_reflection_succeeded_at = nowIso; diff --git a/src/cli/reflection-step-count.test.ts b/src/cli/reflection-step-count.test.ts new file mode 100644 index 0000000000..5608afac9d --- /dev/null +++ b/src/cli/reflection-step-count.test.ts @@ -0,0 +1,276 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import type { LettaStreamingResponse } from "@letta-ai/letta-client/resources/agents/messages"; +import { + createBuffers, + type Line, + markCurrentLineAsFinished, + onChunk, + toLines, +} from "@/cli/helpers/accumulator"; +import { + appendExternalTranscriptEntries, + appendTranscriptDeltaJsonl, + buildAutoReflectionPayload, + finalizeAutoReflectionPayload, + getReflectionTranscriptPaths, + getReflectionTranscriptState, + REFLECTION_STATE_SCHEMA_VERSION, +} from "@/cli/helpers/reflection-transcript"; + +const agentId = "agent-step-count"; +const conversationId = "conversation-step-count"; + +function assistant( + id: string, + text: string, + messageId?: string, +): Extract { + return { + kind: "assistant", + id, + text, + phase: "finished", + messageId, + }; +} + +describe("reflection canonical assistant steps", () => { + let testRoot: string; + + beforeEach(async () => { + testRoot = await mkdtemp(join(tmpdir(), "letta-reflection-steps-")); + process.env.LETTA_TRANSCRIPT_ROOT = testRoot; + }); + + afterEach(async () => { + delete process.env.LETTA_TRANSCRIPT_ROOT; + await rm(testRoot, { recursive: true, force: true }); + }); + + test("assistant display rows split by the real accumulator count as one step", async () => { + const buffers = createBuffers(); + buffers.tokenStreamingEnabled = true; + onChunk(buffers, { + message_type: "assistant_message", + id: "message-a1", + otid: "assistant-otid-a1", + content: `${"A".repeat(1500)}\n\nSecond paragraph`, + } as LettaStreamingResponse); + markCurrentLineAsFinished(buffers); + const lines = toLines(buffers); + const assistantLines = lines.filter((line) => line.kind === "assistant"); + expect(assistantLines).toHaveLength(2); + expect(assistantLines.map((line) => line.messageId)).toEqual([ + "message-a1", + "message-a1", + ]); + + await appendTranscriptDeltaJsonl(agentId, conversationId, lines); + + const state = await getReflectionTranscriptState(agentId, conversationId); + expect(state.total_completed_steps).toBe(1); + expect(state.steps_since_last_successful_reflection).toBe(1); + }); + + test("external ingestion filters repeated source ids before incrementing state", async () => { + const entries = [ + { + kind: "assistant" as const, + text: "external answer", + source_message_id: "external-a1", + }, + ]; + expect( + await appendExternalTranscriptEntries(agentId, conversationId, entries), + ).toEqual({ appended: 1, skipped: 0 }); + expect( + await appendExternalTranscriptEntries(agentId, conversationId, entries), + ).toEqual({ appended: 0, skipped: 1 }); + + const state = await getReflectionTranscriptState(agentId, conversationId); + expect(state.total_completed_steps).toBe(1); + }); + + test("assistant rows with distinct canonical message ids count separately", async () => { + await appendTranscriptDeltaJsonl(agentId, conversationId, [ + assistant("a1", "first", "message-a1"), + assistant("a2", "second", "message-a2"), + ]); + + const state = await getReflectionTranscriptState(agentId, conversationId); + expect(state.total_completed_steps).toBe(2); + }); + + test("assistant rows without canonical message ids retain one-row-per-step fallback", async () => { + await appendTranscriptDeltaJsonl(agentId, conversationId, [ + assistant("local-a1", "first"), + ]); + await appendTranscriptDeltaJsonl(agentId, conversationId, [ + assistant("local-a2", "second"), + ]); + + const state = await getReflectionTranscriptState(agentId, conversationId); + expect(state.total_completed_steps).toBe(2); + }); + + test("v3 state migrates canonical counts from the transcript and duplicate anchor", async () => { + await appendTranscriptDeltaJsonl(agentId, conversationId, [ + { kind: "user", id: "u1", text: "first", messageId: "message-u1" }, + assistant("a1-split-0", "first fragment", "message-a1"), + assistant("a1", "second fragment", "message-a1"), + { kind: "user", id: "u2", text: "second", messageId: "message-u2" }, + assistant("a2", "second answer", "message-a2"), + ]); + const paths = getReflectionTranscriptPaths(agentId, conversationId); + await writeFile( + paths.statePath, + `${JSON.stringify({ + schema_version: "v3_assistant_steps", + reflected_through_message_id: "message-a1", + total_completed_steps: 3, + reflected_completed_steps: 1, + steps_since_last_successful_reflection: 2, + })}\n`, + "utf-8", + ); + + const state = await getReflectionTranscriptState(agentId, conversationId); + expect(state).toMatchObject({ + schema_version: REFLECTION_STATE_SCHEMA_VERSION, + reflected_through_message_id: "message-a1", + total_completed_steps: 2, + reflected_completed_steps: 1, + steps_since_last_successful_reflection: 1, + }); + }); + + test("migration clears a missing anchor before its id appears again", async () => { + await appendTranscriptDeltaJsonl(agentId, conversationId, [ + { kind: "user", id: "u1", text: "old prompt", messageId: "message-u1" }, + assistant("a1", "old unreflected answer", "message-a1"), + ]); + const paths = getReflectionTranscriptPaths(agentId, conversationId); + await writeFile( + paths.statePath, + `${JSON.stringify({ + schema_version: "v3_assistant_steps", + reflected_through_message_id: "message-missing", + total_completed_steps: 1, + reflected_completed_steps: 1, + steps_since_last_successful_reflection: 0, + })}\n`, + "utf-8", + ); + + const migrated = await getReflectionTranscriptState( + agentId, + conversationId, + ); + expect(migrated.reflected_through_message_id).toBeUndefined(); + expect(migrated.reflected_completed_steps).toBe(0); + + await appendTranscriptDeltaJsonl(agentId, conversationId, [ + { + kind: "user", + id: "u2", + text: "new prompt", + messageId: "message-missing", + }, + assistant("a2", "new answer", "message-a2"), + ]); + const payload = await buildAutoReflectionPayload(agentId, conversationId); + expect(payload).not.toBeNull(); + if (!payload) return; + const payloadText = await readFile(payload.payloadPath, "utf-8"); + expect(payloadText).toContain("old unreflected answer"); + expect(payloadText).toContain("new answer"); + }); + + test("a reflected anchor advances through every row sharing its canonical id", async () => { + await appendTranscriptDeltaJsonl(agentId, conversationId, [ + { kind: "user", id: "u1", text: "prompt", messageId: "message-u1" }, + assistant("a1-split-0", "first fragment", "message-a1"), + assistant("a1", "second fragment", "message-a1"), + ]); + + const firstPayload = await buildAutoReflectionPayload( + agentId, + conversationId, + ); + expect(firstPayload).not.toBeNull(); + if (!firstPayload) return; + const firstMessages = JSON.parse( + await readFile(firstPayload.payloadPath, "utf-8"), + ) as Array<{ role: string; content?: string }>; + expect( + firstMessages + .filter((message) => message.role === "assistant") + .map((message) => message.content), + ).toEqual(["first fragment", "second fragment"]); + + await finalizeAutoReflectionPayload( + agentId, + conversationId, + firstPayload.payloadPath, + firstPayload.endSnapshotLine, + true, + ); + await appendTranscriptDeltaJsonl(agentId, conversationId, [ + { kind: "user", id: "u2", text: "next", messageId: "message-u2" }, + assistant("a2", "next answer", "message-a2"), + ]); + + const nextPayload = await buildAutoReflectionPayload( + agentId, + conversationId, + ); + expect(nextPayload).not.toBeNull(); + expect(nextPayload?.startMessageId).toBe("message-u2"); + if (!nextPayload) return; + const nextPayloadText = await readFile(nextPayload.payloadPath, "utf-8"); + expect(nextPayloadText).not.toContain("second fragment"); + }); + + test("new split-row deltas after migration preserve canonical deduplication", async () => { + const paths = getReflectionTranscriptPaths(agentId, conversationId); + await mkdir(paths.rootDir, { recursive: true }); + await writeFile( + paths.transcriptPath, + `${JSON.stringify({ + kind: "assistant", + text: "first fragment", + captured_at: new Date().toISOString(), + source_line_id: "a1-split-0", + source_message_id: "message-a1", + })}\n`, + "utf-8", + ); + await writeFile( + paths.statePath, + `${JSON.stringify({ + schema_version: "v3_assistant_steps", + total_completed_steps: 1, + reflected_completed_steps: 0, + steps_since_last_successful_reflection: 1, + })}\n`, + "utf-8", + ); + + await getReflectionTranscriptState(agentId, conversationId); + await appendTranscriptDeltaJsonl(agentId, conversationId, [ + assistant("a2-split-0", "first fragment", "message-a2"), + assistant("a2", "second fragment", "message-a2"), + ]); + let state = await getReflectionTranscriptState(agentId, conversationId); + expect(state.total_completed_steps).toBe(2); + + await appendTranscriptDeltaJsonl(agentId, conversationId, [ + assistant("a3", "new answer", "message-a3"), + ]); + state = await getReflectionTranscriptState(agentId, conversationId); + expect(state.total_completed_steps).toBe(3); + }); +}); diff --git a/src/test-utils/headless-reflection-scenario.ts b/src/test-utils/headless-reflection-scenario.ts index b5fb46ff99..5c889eb37d 100644 --- a/src/test-utils/headless-reflection-scenario.ts +++ b/src/test-utils/headless-reflection-scenario.ts @@ -537,7 +537,7 @@ function assertScenario(summary: LiveReflectionSummary): void { `Expected at least one auto reflection payload.\n${details}`, ); assertTrue( - summary.state?.schema_version === "v3_assistant_steps", + summary.state?.schema_version === "v4_canonical_assistant_steps", `Unexpected reflection state schema.\n${details}`, ); assertTrue(