@stigmer/sdk 3.12.6 → 3.12.7

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.
Files changed (50) hide show
  1. package/__tests__/update-input-roundtrip.test.js +4 -0
  2. package/__tests__/update-input-roundtrip.test.js.map +1 -1
  3. package/execution/__tests__/transcript.test.d.ts +2 -0
  4. package/execution/__tests__/transcript.test.d.ts.map +1 -0
  5. package/execution/__tests__/transcript.test.js +527 -0
  6. package/execution/__tests__/transcript.test.js.map +1 -0
  7. package/execution/conversation-rules.d.ts +64 -0
  8. package/execution/conversation-rules.d.ts.map +1 -0
  9. package/execution/conversation-rules.js +113 -0
  10. package/execution/conversation-rules.js.map +1 -0
  11. package/execution/transcript.d.ts +171 -0
  12. package/execution/transcript.d.ts.map +1 -0
  13. package/execution/transcript.js +484 -0
  14. package/execution/transcript.js.map +1 -0
  15. package/gen/agentexecution.d.ts +11 -0
  16. package/gen/agentexecution.d.ts.map +1 -1
  17. package/gen/agentexecution.js +30 -1
  18. package/gen/agentexecution.js.map +1 -1
  19. package/gen/client.d.ts +5 -1
  20. package/gen/client.d.ts.map +1 -1
  21. package/gen/client.js +4 -0
  22. package/gen/client.js.map +1 -1
  23. package/gen/identityaccount.d.ts +1 -0
  24. package/gen/identityaccount.d.ts.map +1 -1
  25. package/gen/identityaccount.js +2 -0
  26. package/gen/identityaccount.js.map +1 -1
  27. package/gen/memory.d.ts +58 -0
  28. package/gen/memory.d.ts.map +1 -0
  29. package/gen/memory.js +143 -0
  30. package/gen/memory.js.map +1 -0
  31. package/gen/organization.d.ts +1 -0
  32. package/gen/organization.d.ts.map +1 -1
  33. package/gen/organization.js +2 -0
  34. package/gen/organization.js.map +1 -1
  35. package/index.d.ts +3 -0
  36. package/index.d.ts.map +1 -1
  37. package/index.js +3 -0
  38. package/index.js.map +1 -1
  39. package/package.json +2 -2
  40. package/src/__tests__/update-input-roundtrip.test.ts +4 -0
  41. package/src/execution/__tests__/transcript.golden.md +100 -0
  42. package/src/execution/__tests__/transcript.test.ts +627 -0
  43. package/src/execution/conversation-rules.ts +119 -0
  44. package/src/execution/transcript.ts +735 -0
  45. package/src/gen/agentexecution.ts +45 -1
  46. package/src/gen/client.ts +6 -1
  47. package/src/gen/identityaccount.ts +3 -0
  48. package/src/gen/memory.ts +164 -0
  49. package/src/gen/organization.ts +3 -0
  50. package/src/index.ts +28 -0
@@ -0,0 +1,627 @@
1
+ // The canonical session transcript (stigmer/stigmer#814): assembly rules,
2
+ // offload resolution, and the two serializations.
3
+ //
4
+ // The Markdown format is a pinned contract: transcript.golden.md freezes the
5
+ // exact rendering of a transcript exercising every construct (thinking,
6
+ // system, tool calls with args/results, offloaded outputs resolved and
7
+ // noted, sub-agent nesting, build-from-plan and in-progress markers). A
8
+ // deliberate format change regenerates the golden with
9
+ // UPDATE_TRANSCRIPT_GOLDEN=1 npx vitest run src/execution/__tests__/transcript.test.ts
10
+ // and reviews the diff; an accidental one fails here.
11
+
12
+ import { readFileSync, writeFileSync } from "node:fs";
13
+ import { fileURLToPath } from "node:url";
14
+ import { dirname, resolve } from "node:path";
15
+ import { describe, it, expect } from "vitest";
16
+ import { create } from "@bufbuild/protobuf";
17
+ import type { JsonObject } from "@bufbuild/protobuf";
18
+ import {
19
+ AgentExecutionSchema,
20
+ type AgentExecution,
21
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
22
+ import {
23
+ ExecutionPhase,
24
+ MessageType,
25
+ ToolCallStatus,
26
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
27
+ import {
28
+ AgentExecutionListSchema,
29
+ GetArtifactContentResponseSchema,
30
+ type GetArtifactContentRequest,
31
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/io_pb";
32
+ import { SessionSchema } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
33
+ import {
34
+ assembleSessionTranscript,
35
+ fetchSessionTranscript,
36
+ resolveOffloadedOutputs,
37
+ transcriptToJson,
38
+ transcriptToMarkdown,
39
+ type ResolvedToolOutput,
40
+ } from "../transcript";
41
+
42
+ const here = dirname(fileURLToPath(import.meta.url));
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // Fixture builders
46
+ // ---------------------------------------------------------------------------
47
+
48
+ function session() {
49
+ return create(SessionSchema, {
50
+ metadata: { id: "ses_01" },
51
+ spec: { subject: "Fix the flaky test", agentInstanceId: "agi_01" },
52
+ });
53
+ }
54
+
55
+ /** Turn 1: prompt echo dedupe, thinking, tool call with args + inline
56
+ * result + duration, offloaded text output, image output, raw system. */
57
+ function exec1(): AgentExecution {
58
+ return create(AgentExecutionSchema, {
59
+ metadata: { id: "aex_01a" },
60
+ spec: { sessionId: "ses_01", message: "Why is CI red?" },
61
+ status: {
62
+ phase: ExecutionPhase.EXECUTION_COMPLETED,
63
+ startedAt: "2026-08-20T10:00:00Z",
64
+ completedAt: "2026-08-20T10:01:00Z",
65
+ messages: [
66
+ {
67
+ type: MessageType.MESSAGE_HUMAN,
68
+ content: "Why is CI red?",
69
+ timestamp: "2026-08-20T10:00:01Z",
70
+ },
71
+ {
72
+ type: MessageType.MESSAGE_THINKING,
73
+ content: "The failure is in the retry loop.\nLet me check.",
74
+ timestamp: "2026-08-20T10:00:02Z",
75
+ },
76
+ {
77
+ type: MessageType.MESSAGE_AI,
78
+ content: "Let me look.",
79
+ timestamp: "2026-08-20T10:00:04Z",
80
+ toolCalls: [
81
+ {
82
+ id: "tc_1",
83
+ name: "shell_command",
84
+ args: { command: "go test ./..." } as JsonObject,
85
+ result: "FAIL: TestRetry",
86
+ status: ToolCallStatus.TOOL_CALL_COMPLETED,
87
+ startedAt: "2026-08-20T10:00:05Z",
88
+ completedAt: "2026-08-20T10:00:07Z",
89
+ },
90
+ ],
91
+ },
92
+ {
93
+ type: MessageType.MESSAGE_AI,
94
+ content: "",
95
+ timestamp: "2026-08-20T10:00:10Z",
96
+ toolCalls: [
97
+ {
98
+ id: "tc_2",
99
+ name: "read_file",
100
+ args: { path: "retry.go" } as JsonObject,
101
+ status: ToolCallStatus.TOOL_CALL_COMPLETED,
102
+ outputRef: {
103
+ storageKey: "artifacts/aex_01a/toolcalls/tc_2.txt",
104
+ sizeBytes: 2048n,
105
+ mimeType: "text/plain",
106
+ truncatedPreview: "package retry…",
107
+ },
108
+ },
109
+ {
110
+ id: "tc_3",
111
+ name: "screenshot",
112
+ status: ToolCallStatus.TOOL_CALL_COMPLETED,
113
+ outputRef: {
114
+ storageKey: "artifacts/aex_01a/toolcalls/tc_3.png",
115
+ sizeBytes: 4096n,
116
+ mimeType: "image/png",
117
+ isImage: true,
118
+ },
119
+ },
120
+ ],
121
+ },
122
+ {
123
+ type: MessageType.MESSAGE_SYSTEM,
124
+ content: "Approval received",
125
+ timestamp: "2026-08-20T10:00:20Z",
126
+ },
127
+ ],
128
+ },
129
+ });
130
+ }
131
+
132
+ /** Superseded by exec3's edit-and-resubmit — excluded by default. */
133
+ function exec2(): AgentExecution {
134
+ return create(AgentExecutionSchema, {
135
+ metadata: { id: "aex_01b" },
136
+ spec: { sessionId: "ses_01", message: "old prompt" },
137
+ status: { phase: ExecutionPhase.EXECUTION_COMPLETED },
138
+ });
139
+ }
140
+
141
+ /** Turn 2: edit-and-resubmit successor with a sub-agent delegation. */
142
+ function exec3(): AgentExecution {
143
+ return create(AgentExecutionSchema, {
144
+ metadata: { id: "aex_01c" },
145
+ spec: {
146
+ sessionId: "ses_01",
147
+ message: "edited prompt",
148
+ supersedesExecutionId: "aex_01b",
149
+ },
150
+ status: {
151
+ phase: ExecutionPhase.EXECUTION_COMPLETED,
152
+ startedAt: "2026-08-20T11:00:00Z",
153
+ messages: [
154
+ {
155
+ type: MessageType.MESSAGE_AI,
156
+ content: "Delegating.",
157
+ toolCalls: [
158
+ {
159
+ id: "tc_sa",
160
+ name: "task",
161
+ status: ToolCallStatus.TOOL_CALL_COMPLETED,
162
+ },
163
+ ],
164
+ },
165
+ ],
166
+ subAgentExecutions: [
167
+ {
168
+ id: "tc_sa",
169
+ name: "explore",
170
+ subject: "Explore the repo",
171
+ input: "find tests",
172
+ output: "found 3",
173
+ startedAt: "2026-08-20T11:00:01Z",
174
+ completedAt: "2026-08-20T11:00:06Z",
175
+ messages: [
176
+ {
177
+ type: MessageType.MESSAGE_AI,
178
+ content: "Scanning.",
179
+ toolCalls: [
180
+ {
181
+ id: "tc_sa_1",
182
+ name: "grep",
183
+ result: "3 matches",
184
+ status: ToolCallStatus.TOOL_CALL_COMPLETED,
185
+ outputRef: {
186
+ // Sub-agent outputs are stored under the PARENT
187
+ // execution's id — the storage key is the record of it.
188
+ storageKey: "artifacts/aex_01c/toolcalls/tc_sa_1.txt",
189
+ sizeBytes: 64n,
190
+ mimeType: "text/plain",
191
+ truncatedPreview: "3 matches…",
192
+ },
193
+ },
194
+ ],
195
+ },
196
+ ],
197
+ },
198
+ ],
199
+ },
200
+ });
201
+ }
202
+
203
+ /** Turn 3: an in-flight Build-from-plan turn. */
204
+ function exec4(): AgentExecution {
205
+ return create(AgentExecutionSchema, {
206
+ metadata: { id: "aex_01d" },
207
+ spec: {
208
+ sessionId: "ses_01",
209
+ message: "Build from plan",
210
+ executionConfig: { buildFromPlan: true },
211
+ },
212
+ status: {
213
+ phase: ExecutionPhase.EXECUTION_IN_PROGRESS,
214
+ startedAt: "2026-08-20T12:00:00Z",
215
+ messages: [{ type: MessageType.MESSAGE_AI, content: "Working on it." }],
216
+ },
217
+ });
218
+ }
219
+
220
+ function allExecutions(): AgentExecution[] {
221
+ // Deliberately scrambled: assembly must restore ULID order.
222
+ return [exec3(), exec1(), exec4(), exec2()];
223
+ }
224
+
225
+ const RESOLVED: Record<string, ResolvedToolOutput> = {
226
+ "artifacts/aex_01a/toolcalls/tc_2.txt": {
227
+ storageKey: "artifacts/aex_01a/toolcalls/tc_2.txt",
228
+ content: "package retry\n\nfunc Do() {}",
229
+ truncated: false,
230
+ totalSizeBytes: 2048,
231
+ mimeType: "text/plain",
232
+ isImage: false,
233
+ },
234
+ "artifacts/aex_01a/toolcalls/tc_3.png": {
235
+ storageKey: "artifacts/aex_01a/toolcalls/tc_3.png",
236
+ truncated: false,
237
+ totalSizeBytes: 4096,
238
+ mimeType: "image/png",
239
+ isImage: true,
240
+ },
241
+ "artifacts/aex_01c/toolcalls/tc_sa_1.txt": {
242
+ storageKey: "artifacts/aex_01c/toolcalls/tc_sa_1.txt",
243
+ content: "3 matches in test/",
244
+ truncated: false,
245
+ totalSizeBytes: 64,
246
+ mimeType: "text/plain",
247
+ isImage: false,
248
+ },
249
+ };
250
+
251
+ // ---------------------------------------------------------------------------
252
+ // Assembly
253
+ // ---------------------------------------------------------------------------
254
+
255
+ describe("assembleSessionTranscript", () => {
256
+ it("orders turns chronologically by ULID regardless of input order", () => {
257
+ const t = assembleSessionTranscript(session(), allExecutions());
258
+ expect(t.turns.map((x) => x.execution.metadata?.id)).toEqual([
259
+ "aex_01a",
260
+ "aex_01c",
261
+ "aex_01d",
262
+ ]);
263
+ });
264
+
265
+ it("excludes superseded turns by default, matching the conversation view", () => {
266
+ const t = assembleSessionTranscript(session(), allExecutions());
267
+ expect(
268
+ t.turns.some((x) => x.execution.metadata?.id === "aex_01b"),
269
+ ).toBe(false);
270
+ expect(t.includesSuperseded).toBe(false);
271
+ });
272
+
273
+ it("keeps superseded turns, marked, when includeSuperseded is set", () => {
274
+ const t = assembleSessionTranscript(session(), allExecutions(), {
275
+ includeSuperseded: true,
276
+ });
277
+ const ids = t.turns.map((x) => x.execution.metadata?.id);
278
+ expect(ids).toEqual(["aex_01a", "aex_01b", "aex_01c", "aex_01d"]);
279
+ expect(t.turns.map((x) => x.superseded)).toEqual([
280
+ false,
281
+ true,
282
+ false,
283
+ false,
284
+ ]);
285
+ });
286
+
287
+ it("synthesizes user prompts by the shared rule", () => {
288
+ const t = assembleSessionTranscript(session(), allExecutions());
289
+ expect(t.turns[0].userPrompt).toBe("Why is CI red?");
290
+ // Build-from-plan: machine label, no user prose.
291
+ expect(t.turns[2].userPrompt).toBeNull();
292
+ expect(t.turns[2].isBuildFromPlan).toBe(true);
293
+ });
294
+
295
+ it("suppresses the 'execute' placeholder prompt", () => {
296
+ const exec = create(AgentExecutionSchema, {
297
+ metadata: { id: "aex_x" },
298
+ spec: { sessionId: "ses_01", message: "execute" },
299
+ status: { phase: ExecutionPhase.EXECUTION_COMPLETED },
300
+ });
301
+ const t = assembleSessionTranscript(session(), [exec]);
302
+ expect(t.turns[0].userPrompt).toBeNull();
303
+ expect(t.turns[0].isBuildFromPlan).toBe(false);
304
+ });
305
+
306
+ it("marks non-terminal executions in progress", () => {
307
+ const t = assembleSessionTranscript(session(), allExecutions());
308
+ expect(t.turns.map((x) => x.inProgress)).toEqual([false, false, true]);
309
+ });
310
+ });
311
+
312
+ // ---------------------------------------------------------------------------
313
+ // Offload resolution
314
+ // ---------------------------------------------------------------------------
315
+
316
+ type ContentByKey = Record<
317
+ string,
318
+ { content: string; truncated?: boolean; totalSizeBytes?: number } | Error
319
+ >;
320
+
321
+ function fakeArtifactClient(contentByKey: ContentByKey) {
322
+ const requests: GetArtifactContentRequest[] = [];
323
+ return {
324
+ requests,
325
+ client: {
326
+ agentExecution: {
327
+ listBySession: () => Promise.reject(new Error("not under test")),
328
+ getArtifactContent: (input: GetArtifactContentRequest) => {
329
+ requests.push(input);
330
+ const entry = contentByKey[input.storageKey];
331
+ if (entry === undefined) {
332
+ return Promise.reject(new Error(`no such key: ${input.storageKey}`));
333
+ }
334
+ if (entry instanceof Error) return Promise.reject(entry);
335
+ return Promise.resolve(
336
+ create(GetArtifactContentResponseSchema, {
337
+ content: new TextEncoder().encode(entry.content),
338
+ truncated: entry.truncated ?? false,
339
+ totalSizeBytes: BigInt(entry.totalSizeBytes ?? entry.content.length),
340
+ }),
341
+ );
342
+ },
343
+ },
344
+ },
345
+ };
346
+ }
347
+
348
+ describe("resolveOffloadedOutputs", () => {
349
+ it("resolves parent and sub-agent refs, deriving each execution id from the storage key", async () => {
350
+ const { client, requests } = fakeArtifactClient({
351
+ "artifacts/aex_01a/toolcalls/tc_2.txt": { content: "full file" },
352
+ "artifacts/aex_01c/toolcalls/tc_sa_1.txt": { content: "3 matches in test/" },
353
+ });
354
+ const resolved = await resolveOffloadedOutputs(client, allExecutions());
355
+
356
+ expect(resolved["artifacts/aex_01a/toolcalls/tc_2.txt"].content).toBe(
357
+ "full file",
358
+ );
359
+ // The sub-agent's ref (nested in exec3's sub_agent_executions) resolved
360
+ // under the PARENT execution's id, taken from the key itself.
361
+ const subAgentRequest = requests.find(
362
+ (r) => r.storageKey === "artifacts/aex_01c/toolcalls/tc_sa_1.txt",
363
+ );
364
+ expect(subAgentRequest?.executionId).toBe("aex_01c");
365
+ });
366
+
367
+ it("never fetches image refs, but records them for the serializers", async () => {
368
+ const { client, requests } = fakeArtifactClient({
369
+ "artifacts/aex_01a/toolcalls/tc_2.txt": { content: "full file" },
370
+ "artifacts/aex_01c/toolcalls/tc_sa_1.txt": { content: "x" },
371
+ });
372
+ const resolved = await resolveOffloadedOutputs(client, allExecutions());
373
+
374
+ const imageKey = "artifacts/aex_01a/toolcalls/tc_3.png";
375
+ expect(requests.some((r) => r.storageKey === imageKey)).toBe(false);
376
+ expect(resolved[imageKey]).toMatchObject({
377
+ isImage: true,
378
+ mimeType: "image/png",
379
+ totalSizeBytes: 4096,
380
+ });
381
+ });
382
+
383
+ it("tolerates individual fetch failures — the rest still resolve", async () => {
384
+ const { client } = fakeArtifactClient({
385
+ "artifacts/aex_01a/toolcalls/tc_2.txt": new Error("storage unavailable"),
386
+ "artifacts/aex_01c/toolcalls/tc_sa_1.txt": { content: "3 matches in test/" },
387
+ });
388
+ const resolved = await resolveOffloadedOutputs(client, allExecutions());
389
+
390
+ expect(resolved["artifacts/aex_01a/toolcalls/tc_2.txt"].error).toBe(
391
+ "storage unavailable",
392
+ );
393
+ expect(resolved["artifacts/aex_01a/toolcalls/tc_2.txt"].content).toBeUndefined();
394
+ expect(
395
+ resolved["artifacts/aex_01c/toolcalls/tc_sa_1.txt"].content,
396
+ ).toBe("3 matches in test/");
397
+ });
398
+
399
+ it("carries the server's truncation flag and true size", async () => {
400
+ const { client } = fakeArtifactClient({
401
+ "artifacts/aex_01a/toolcalls/tc_2.txt": {
402
+ content: "first half…",
403
+ truncated: true,
404
+ totalSizeBytes: 1048576,
405
+ },
406
+ "artifacts/aex_01c/toolcalls/tc_sa_1.txt": { content: "x" },
407
+ });
408
+ const resolved = await resolveOffloadedOutputs(client, allExecutions());
409
+ expect(resolved["artifacts/aex_01a/toolcalls/tc_2.txt"]).toMatchObject({
410
+ truncated: true,
411
+ totalSizeBytes: 1048576,
412
+ });
413
+ });
414
+ });
415
+
416
+ // ---------------------------------------------------------------------------
417
+ // Fetch
418
+ // ---------------------------------------------------------------------------
419
+
420
+ describe("fetchSessionTranscript", () => {
421
+ function fakeClient(overrides?: { totalPages?: number }) {
422
+ const artifacts = fakeArtifactClient({
423
+ "artifacts/aex_01a/toolcalls/tc_2.txt": { content: "full file" },
424
+ "artifacts/aex_01c/toolcalls/tc_sa_1.txt": { content: "3 matches" },
425
+ });
426
+ return {
427
+ session: { get: (id: string) => Promise.resolve(session()) },
428
+ agentExecution: {
429
+ ...artifacts.client.agentExecution,
430
+ listBySession: () =>
431
+ Promise.resolve(
432
+ create(AgentExecutionListSchema, {
433
+ totalPages: overrides?.totalPages ?? 1,
434
+ entries: allExecutions(),
435
+ }),
436
+ ),
437
+ },
438
+ };
439
+ }
440
+
441
+ it("assembles the full transcript with outputs resolved", async () => {
442
+ const t = await fetchSessionTranscript(fakeClient(), "ses_01");
443
+ expect(t.turns).toHaveLength(3);
444
+ expect(
445
+ t.resolvedOutputs["artifacts/aex_01a/toolcalls/tc_2.txt"].content,
446
+ ).toBe("full file");
447
+ });
448
+
449
+ it("skips output resolution when disabled", async () => {
450
+ const t = await fetchSessionTranscript(fakeClient(), "ses_01", {
451
+ resolveOutputs: false,
452
+ });
453
+ expect(Object.keys(t.resolvedOutputs)).toHaveLength(0);
454
+ });
455
+
456
+ it("refuses to export a silently truncated conversation if the server ever paginates", async () => {
457
+ await expect(
458
+ fetchSessionTranscript(fakeClient({ totalPages: 2 }), "ses_01"),
459
+ ).rejects.toThrow(/2 pages .* single page/s);
460
+ });
461
+ });
462
+
463
+ // ---------------------------------------------------------------------------
464
+ // Markdown
465
+ // ---------------------------------------------------------------------------
466
+
467
+ describe("transcriptToMarkdown", () => {
468
+ it("matches the pinned format contract (transcript.golden.md)", () => {
469
+ const t = assembleSessionTranscript(session(), allExecutions(), {
470
+ resolvedOutputs: RESOLVED,
471
+ });
472
+ const goldenPath = resolve(here, "transcript.golden.md");
473
+ const actual = transcriptToMarkdown(t);
474
+ if (process.env.UPDATE_TRANSCRIPT_GOLDEN) {
475
+ writeFileSync(goldenPath, actual);
476
+ }
477
+ expect(actual).toBe(readFileSync(goldenPath, "utf8"));
478
+ });
479
+
480
+ it("stamps the export time only when provided", () => {
481
+ const t = assembleSessionTranscript(session(), [exec1()]);
482
+ expect(transcriptToMarkdown(t)).not.toContain("Exported:");
483
+ expect(
484
+ transcriptToMarkdown(t, { generatedAt: "2026-08-22T06:00:00Z" }),
485
+ ).toContain("- Exported: 2026-08-22T06:00:00Z");
486
+ });
487
+
488
+ it("falls back to the ref's preview, with an honest note, when an output is unresolved", () => {
489
+ const t = assembleSessionTranscript(session(), [exec1()]);
490
+ const md = transcriptToMarkdown(t);
491
+ expect(md).toContain("_Offloaded output not resolved — showing preview._");
492
+ expect(md).toContain("package retry…");
493
+ });
494
+
495
+ it("notes the fetch failure when resolution was attempted and failed", () => {
496
+ const t = assembleSessionTranscript(session(), [exec1()], {
497
+ resolvedOutputs: {
498
+ "artifacts/aex_01a/toolcalls/tc_2.txt": {
499
+ storageKey: "artifacts/aex_01a/toolcalls/tc_2.txt",
500
+ truncated: false,
501
+ mimeType: "text/plain",
502
+ isImage: false,
503
+ error: "storage unavailable",
504
+ },
505
+ },
506
+ });
507
+ expect(transcriptToMarkdown(t)).toContain(
508
+ "_Offloaded output unavailable (storage unavailable) — showing preview._",
509
+ );
510
+ });
511
+
512
+ it("marks server-truncated outputs with the byte counts", () => {
513
+ const t = assembleSessionTranscript(session(), [exec1()], {
514
+ resolvedOutputs: {
515
+ "artifacts/aex_01a/toolcalls/tc_2.txt": {
516
+ storageKey: "artifacts/aex_01a/toolcalls/tc_2.txt",
517
+ content: "0123456789",
518
+ truncated: true,
519
+ totalSizeBytes: 1048576,
520
+ mimeType: "text/plain",
521
+ isImage: false,
522
+ },
523
+ },
524
+ });
525
+ expect(transcriptToMarkdown(t)).toContain(
526
+ "_Output truncated at 10 of 1048576 bytes (server content cap)._",
527
+ );
528
+ });
529
+
530
+ it("sizes fences past backtick runs in the content", () => {
531
+ const exec = create(AgentExecutionSchema, {
532
+ metadata: { id: "aex_x" },
533
+ spec: { sessionId: "ses_01", message: "prompt" },
534
+ status: {
535
+ phase: ExecutionPhase.EXECUTION_COMPLETED,
536
+ messages: [
537
+ {
538
+ type: MessageType.MESSAGE_AI,
539
+ content: "",
540
+ toolCalls: [
541
+ {
542
+ id: "tc",
543
+ name: "shell_command",
544
+ result: "a fence: ```md\ninside\n```",
545
+ status: ToolCallStatus.TOOL_CALL_COMPLETED,
546
+ },
547
+ ],
548
+ },
549
+ ],
550
+ },
551
+ });
552
+ const md = transcriptToMarkdown(
553
+ assembleSessionTranscript(session(), [exec]),
554
+ );
555
+ expect(md).toContain("````\na fence: ```md\ninside\n```\n````");
556
+ });
557
+
558
+ it("marks kept superseded turns", () => {
559
+ const t = assembleSessionTranscript(session(), allExecutions(), {
560
+ includeSuperseded: true,
561
+ });
562
+ const md = transcriptToMarkdown(t);
563
+ expect(md).toContain("- Includes superseded (edited-and-resubmitted) turns");
564
+ expect(md).toContain("_Superseded by an edited resubmission._");
565
+ expect(md).toContain("old prompt");
566
+ });
567
+ });
568
+
569
+ // ---------------------------------------------------------------------------
570
+ // JSON
571
+ // ---------------------------------------------------------------------------
572
+
573
+ describe("transcriptToJson", () => {
574
+ it("is JSON.stringify-safe despite bigint proto fields (protojson contract)", () => {
575
+ const t = assembleSessionTranscript(session(), allExecutions(), {
576
+ resolvedOutputs: RESOLVED,
577
+ });
578
+ // sizeBytes is int64 → bigint on the proto; a raw stringify of the
579
+ // transcript would throw. The projection must not.
580
+ const text = JSON.stringify(transcriptToJson(t), null, 2);
581
+ const parsed = JSON.parse(text);
582
+ expect(parsed.format).toBe("stigmer.ai/session-transcript/v1");
583
+ expect(parsed.session.metadata.id).toBe("ses_01");
584
+ expect(parsed.turns).toHaveLength(3);
585
+ // protojson: snake_case field names, int64 as string.
586
+ expect(
587
+ parsed.turns[0].execution.status.messages[3].tool_calls[0].output_ref
588
+ .size_bytes,
589
+ ).toBe("2048");
590
+ });
591
+
592
+ it("carries the canonical-rule verdicts per turn", () => {
593
+ const t = assembleSessionTranscript(session(), allExecutions(), {
594
+ includeSuperseded: true,
595
+ });
596
+ const parsed = JSON.parse(JSON.stringify(transcriptToJson(t)));
597
+ expect(parsed.includes_superseded).toBe(true);
598
+ expect(parsed.turns[0].user_prompt).toBe("Why is CI red?");
599
+ expect(parsed.turns[1].superseded).toBe(true);
600
+ expect(parsed.turns[3].build_from_plan).toBe(true);
601
+ expect(parsed.turns[3].in_progress).toBe(true);
602
+ });
603
+
604
+ it("strips the internal Temporal callback token from execution status", () => {
605
+ const exec = exec1();
606
+ exec.status!.callbackToken = new TextEncoder().encode("task-token");
607
+ const t = assembleSessionTranscript(session(), [exec]);
608
+ const parsed = JSON.parse(JSON.stringify(transcriptToJson(t)));
609
+ expect(parsed.turns[0].execution.status.callback_token).toBeUndefined();
610
+ // The strip is surgical — the rest of status is intact.
611
+ expect(parsed.turns[0].execution.status.messages).toHaveLength(5);
612
+ });
613
+
614
+ it("keys resolved outputs by storage key with plain-JSON fields", () => {
615
+ const t = assembleSessionTranscript(session(), allExecutions(), {
616
+ resolvedOutputs: RESOLVED,
617
+ });
618
+ const parsed = JSON.parse(JSON.stringify(transcriptToJson(t)));
619
+ expect(
620
+ parsed.resolved_outputs["artifacts/aex_01a/toolcalls/tc_2.txt"],
621
+ ).toMatchObject({
622
+ content: "package retry\n\nfunc Do() {}",
623
+ mime_type: "text/plain",
624
+ total_size_bytes: 2048,
625
+ });
626
+ });
627
+ });