Skip to content

Commit 15f04f2

Browse files
committed
feat(sdk): route chat.agent transcript persistence through a TranscriptStorage seam
Introduces the TranscriptStorage interface (load/save over id-addressed changes: put, remove, truncateAfter, state) and makes the built-in snapshot writer its default implementation. The runtime keeps a shadow of the transcript it last saved and hands the storage the diff after every turn, failed turn and history-changing action, together with the stream cursors the next boot resumes from. The default storage reduces each changeset onto an in-memory copy and rewrites the blob as version 2, so a turn still costs one PUT and no GET, and the boot read goes through load. The snapshot read/write helpers move to their own module so the storage can import them without a cycle; the test seams keep their import path. The mock chat agent harness now reports version 2 snapshots and accepts either version as a seed.
1 parent d37c018 commit 15f04f2

12 files changed

Lines changed: 1068 additions & 315 deletions

apps/webapp/test/chat-snapshot-integration.test.ts

Lines changed: 17 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,8 @@
1-
// Plan F.3: integration test that round-trips a `ChatSnapshotV1` blob
2-
// through the SDK's snapshot helpers + a real MinIO backing store. Mirrors
3-
// the testcontainer pattern from `objectStore.test.ts`.
4-
//
5-
// What this verifies end-to-end:
6-
// - SDK's `writeChatSnapshot` calls `apiClient.createUploadPayloadUrl`
7-
// to mint a presigned PUT, then PUTs JSON to it.
8-
// - SDK's `readChatSnapshot` calls `apiClient.getPayloadUrl` to mint a
9-
// presigned GET, then fetches and parses.
10-
// - The webapp's `generatePresignedUrl` produces URLs MinIO accepts.
11-
// - The blob round-trips with `version: 1` shape preserved.
12-
// - 404 (no snapshot for a fresh session) returns `undefined`, not an
13-
// error.
14-
//
15-
// This is the integration safety net behind the unit tests in
16-
// `packages/trigger-sdk/test/chat-snapshot.test.ts` — those tests mock
17-
// `fetch`; this one drives a real S3-compatible backend.
18-
191
import { postgresAndMinioTest } from "@internal/testcontainers";
20-
import { apiClientManager } from "@trigger.dev/core/v3";
2+
import { apiClientManager, type TranscriptSnapshotV2 } from "@trigger.dev/core/v3";
213
import {
224
__readChatSnapshotProductionPathForTests as readChatSnapshot,
235
__writeChatSnapshotProductionPathForTests as writeChatSnapshot,
24-
type ChatSnapshotV1,
256
} from "@trigger.dev/sdk/ai";
267
import type { UIMessage } from "ai";
278
import { afterEach, describe, expect, vi } from "vitest";
@@ -35,22 +16,24 @@ vi.setConfig({ testTimeout: 60_000 });
3516

3617
function makeSnapshot(
3718
opts: { messages?: UIMessage[]; lastOutEventId?: string } = {}
38-
): ChatSnapshotV1 {
19+
): TranscriptSnapshotV2 {
20+
const messages = opts.messages ?? [
21+
{
22+
id: "u-1",
23+
role: "user",
24+
parts: [{ type: "text", text: "hello" }],
25+
},
26+
{
27+
id: "a-1",
28+
role: "assistant",
29+
parts: [{ type: "text", text: "world" }],
30+
},
31+
];
3932
return {
40-
version: 1,
33+
version: 2,
4134
savedAt: 1_700_000_000_000,
42-
messages: opts.messages ?? [
43-
{
44-
id: "u-1",
45-
role: "user",
46-
parts: [{ type: "text", text: "hello" }],
47-
},
48-
{
49-
id: "a-1",
50-
role: "assistant",
51-
parts: [{ type: "text", text: "world" }],
52-
},
53-
],
35+
messages: messages.map((message) => ({ id: message.id, final: true, message })),
36+
state: null,
5437
lastOutEventId: opts.lastOutEventId ?? "evt-42",
5538
};
5639
}

apps/webapp/test/replay-after-crash.test.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,10 @@
2424
// through it), even though the replay path itself doesn't read from S3.
2525

2626
import { postgresAndMinioTest } from "@internal/testcontainers";
27-
import { apiClientManager } from "@trigger.dev/core/v3";
27+
import { apiClientManager, type TranscriptSnapshotV2 } from "@trigger.dev/core/v3";
2828
import {
2929
__readChatSnapshotProductionPathForTests as readChatSnapshot,
3030
__replaySessionOutTailProductionPathForTests as replaySessionOutTail,
31-
type ChatSnapshotV1,
3231
} from "@trigger.dev/sdk/ai";
3332
import type { UIMessageChunk } from "ai";
3433
import { afterEach, describe, expect, vi } from "vitest";
@@ -265,13 +264,26 @@ describe("replay after crash (MinIO + SDK helpers)", () => {
265264

266265
// Pre-write a snapshot to MinIO via real apiClient stub.
267266
const sessionId = "sess_merge_round_trip";
268-
const snapshot: ChatSnapshotV1 = {
269-
version: 1,
267+
const snapshot: TranscriptSnapshotV2 = {
268+
version: 2,
270269
savedAt: 1_700_000_000_000,
271270
messages: [
272-
{ id: "u-1", role: "user", parts: [{ type: "text", text: "hi" }] },
273-
{ id: "a-1", role: "assistant", parts: [{ type: "text", text: "stale-assistant" }] },
271+
{
272+
id: "u-1",
273+
final: true,
274+
message: { id: "u-1", role: "user", parts: [{ type: "text", text: "hi" }] },
275+
},
276+
{
277+
id: "a-1",
278+
final: true,
279+
message: {
280+
id: "a-1",
281+
role: "assistant",
282+
parts: [{ type: "text", text: "stale-assistant" }],
283+
},
284+
},
274285
],
286+
state: null,
275287
lastOutEventId: "evt-prev",
276288
};
277289

0 commit comments

Comments
 (0)