@stigmer/sdk 3.12.5 → 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 (175) hide show
  1. package/__tests__/update-input-roundtrip.test.d.ts +2 -0
  2. package/__tests__/update-input-roundtrip.test.d.ts.map +1 -0
  3. package/__tests__/update-input-roundtrip.test.js +795 -0
  4. package/__tests__/update-input-roundtrip.test.js.map +1 -0
  5. package/__tests__/update-input.test.js +22 -3
  6. package/__tests__/update-input.test.js.map +1 -1
  7. package/execution/__tests__/transcript.test.d.ts +2 -0
  8. package/execution/__tests__/transcript.test.d.ts.map +1 -0
  9. package/execution/__tests__/transcript.test.js +527 -0
  10. package/execution/__tests__/transcript.test.js.map +1 -0
  11. package/execution/conversation-rules.d.ts +64 -0
  12. package/execution/conversation-rules.d.ts.map +1 -0
  13. package/execution/conversation-rules.js +113 -0
  14. package/execution/conversation-rules.js.map +1 -0
  15. package/execution/transcript.d.ts +171 -0
  16. package/execution/transcript.d.ts.map +1 -0
  17. package/execution/transcript.js +484 -0
  18. package/execution/transcript.js.map +1 -0
  19. package/gen/agent.d.ts +21 -0
  20. package/gen/agent.d.ts.map +1 -1
  21. package/gen/agent.js +73 -1
  22. package/gen/agent.js.map +1 -1
  23. package/gen/agentchannel.d.ts +23 -1
  24. package/gen/agentchannel.d.ts.map +1 -1
  25. package/gen/agentchannel.js +55 -1
  26. package/gen/agentchannel.js.map +1 -1
  27. package/gen/agentexecution.d.ts +34 -1
  28. package/gen/agentexecution.d.ts.map +1 -1
  29. package/gen/agentexecution.js +176 -2
  30. package/gen/agentexecution.js.map +1 -1
  31. package/gen/agentinstance.d.ts +21 -0
  32. package/gen/agentinstance.d.ts.map +1 -1
  33. package/gen/agentinstance.js +32 -1
  34. package/gen/agentinstance.js.map +1 -1
  35. package/gen/agentshare.d.ts +23 -1
  36. package/gen/agentshare.d.ts.map +1 -1
  37. package/gen/agentshare.js +53 -1
  38. package/gen/agentshare.js.map +1 -1
  39. package/gen/apikey.d.ts +21 -0
  40. package/gen/apikey.d.ts.map +1 -1
  41. package/gen/apikey.js +33 -1
  42. package/gen/apikey.js.map +1 -1
  43. package/gen/artifact.d.ts +8 -0
  44. package/gen/artifact.d.ts.map +1 -1
  45. package/gen/artifact.js +1 -0
  46. package/gen/artifact.js.map +1 -1
  47. package/gen/channelapp.d.ts +21 -0
  48. package/gen/channelapp.d.ts.map +1 -1
  49. package/gen/channelapp.js +45 -0
  50. package/gen/channelapp.js.map +1 -1
  51. package/gen/client.d.ts +5 -1
  52. package/gen/client.d.ts.map +1 -1
  53. package/gen/client.js +4 -0
  54. package/gen/client.js.map +1 -1
  55. package/gen/environment.d.ts +21 -0
  56. package/gen/environment.d.ts.map +1 -1
  57. package/gen/environment.js +31 -1
  58. package/gen/environment.js.map +1 -1
  59. package/gen/executioncontext.d.ts +8 -0
  60. package/gen/executioncontext.d.ts.map +1 -1
  61. package/gen/executioncontext.js +1 -0
  62. package/gen/executioncontext.js.map +1 -1
  63. package/gen/iampolicy.d.ts +8 -0
  64. package/gen/iampolicy.d.ts.map +1 -1
  65. package/gen/iampolicy.js +1 -0
  66. package/gen/iampolicy.js.map +1 -1
  67. package/gen/identityaccount.d.ts +22 -0
  68. package/gen/identityaccount.d.ts.map +1 -1
  69. package/gen/identityaccount.js +48 -1
  70. package/gen/identityaccount.js.map +1 -1
  71. package/gen/identityprovider.d.ts +21 -0
  72. package/gen/identityprovider.d.ts.map +1 -1
  73. package/gen/identityprovider.js +40 -0
  74. package/gen/identityprovider.js.map +1 -1
  75. package/gen/invitation.d.ts +8 -0
  76. package/gen/invitation.d.ts.map +1 -1
  77. package/gen/invitation.js +1 -0
  78. package/gen/invitation.js.map +1 -1
  79. package/gen/mcpserver.d.ts +21 -0
  80. package/gen/mcpserver.d.ts.map +1 -1
  81. package/gen/mcpserver.js +79 -1
  82. package/gen/mcpserver.js.map +1 -1
  83. package/gen/memory.d.ts +58 -0
  84. package/gen/memory.d.ts.map +1 -0
  85. package/gen/memory.js +143 -0
  86. package/gen/memory.js.map +1 -0
  87. package/gen/oauthapp.d.ts +21 -0
  88. package/gen/oauthapp.d.ts.map +1 -1
  89. package/gen/oauthapp.js +39 -0
  90. package/gen/oauthapp.js.map +1 -1
  91. package/gen/organization.d.ts +22 -0
  92. package/gen/organization.d.ts.map +1 -1
  93. package/gen/organization.js +43 -1
  94. package/gen/organization.js.map +1 -1
  95. package/gen/platformclient.d.ts +21 -0
  96. package/gen/platformclient.d.ts.map +1 -1
  97. package/gen/platformclient.js +40 -1
  98. package/gen/platformclient.js.map +1 -1
  99. package/gen/project.d.ts +21 -0
  100. package/gen/project.d.ts.map +1 -1
  101. package/gen/project.js +32 -1
  102. package/gen/project.js.map +1 -1
  103. package/gen/proto-utils.d.ts +36 -0
  104. package/gen/proto-utils.d.ts.map +1 -1
  105. package/gen/proto-utils.js +58 -0
  106. package/gen/proto-utils.js.map +1 -1
  107. package/gen/schedule.d.ts +23 -1
  108. package/gen/schedule.d.ts.map +1 -1
  109. package/gen/schedule.js +79 -1
  110. package/gen/schedule.js.map +1 -1
  111. package/gen/session.d.ts +21 -0
  112. package/gen/session.d.ts.map +1 -1
  113. package/gen/session.js +80 -1
  114. package/gen/session.js.map +1 -1
  115. package/gen/skill.d.ts +8 -0
  116. package/gen/skill.d.ts.map +1 -1
  117. package/gen/skill.js +1 -0
  118. package/gen/skill.js.map +1 -1
  119. package/gen/workflow.d.ts +21 -0
  120. package/gen/workflow.d.ts.map +1 -1
  121. package/gen/workflow.js +79 -0
  122. package/gen/workflow.js.map +1 -1
  123. package/gen/workflowexecution.d.ts +21 -0
  124. package/gen/workflowexecution.d.ts.map +1 -1
  125. package/gen/workflowexecution.js +36 -1
  126. package/gen/workflowexecution.js.map +1 -1
  127. package/gen/workflowinstance.d.ts +21 -0
  128. package/gen/workflowinstance.d.ts.map +1 -1
  129. package/gen/workflowinstance.js +33 -1
  130. package/gen/workflowinstance.js.map +1 -1
  131. package/index.d.ts +23 -21
  132. package/index.d.ts.map +1 -1
  133. package/index.js +27 -23
  134. package/index.js.map +1 -1
  135. package/package.json +2 -2
  136. package/src/__tests__/update-input-roundtrip.test.ts +1035 -0
  137. package/src/__tests__/update-input.test.ts +34 -3
  138. package/src/execution/__tests__/transcript.golden.md +100 -0
  139. package/src/execution/__tests__/transcript.test.ts +627 -0
  140. package/src/execution/conversation-rules.ts +119 -0
  141. package/src/execution/transcript.ts +735 -0
  142. package/src/gen/agent.ts +89 -3
  143. package/src/gen/agentchannel.ts +72 -4
  144. package/src/gen/agentexecution.ts +216 -6
  145. package/src/gen/agentinstance.ts +41 -1
  146. package/src/gen/agentshare.ts +68 -4
  147. package/src/gen/apikey.ts +42 -1
  148. package/src/gen/artifact.ts +9 -0
  149. package/src/gen/channelapp.ts +57 -1
  150. package/src/gen/client.ts +6 -1
  151. package/src/gen/environment.ts +40 -1
  152. package/src/gen/executioncontext.ts +9 -0
  153. package/src/gen/iampolicy.ts +9 -0
  154. package/src/gen/identityaccount.ts +60 -2
  155. package/src/gen/identityprovider.ts +49 -0
  156. package/src/gen/invitation.ts +9 -0
  157. package/src/gen/mcpserver.ts +95 -3
  158. package/src/gen/memory.ts +164 -0
  159. package/src/gen/oauthapp.ts +48 -0
  160. package/src/gen/organization.ts +55 -2
  161. package/src/gen/platformclient.ts +49 -1
  162. package/src/gen/project.ts +41 -1
  163. package/src/gen/proto-utils.ts +64 -0
  164. package/src/gen/schedule.ts +98 -4
  165. package/src/gen/session.ts +97 -3
  166. package/src/gen/skill.ts +9 -0
  167. package/src/gen/workflow.ts +96 -2
  168. package/src/gen/workflowexecution.ts +45 -1
  169. package/src/gen/workflowinstance.ts +42 -1
  170. package/src/index.ts +52 -9
  171. package/src/update-input.ts +0 -168
  172. package/update-input.d.ts +0 -24
  173. package/update-input.d.ts.map +0 -1
  174. package/update-input.js +0 -126
  175. package/update-input.js.map +0 -1
@@ -0,0 +1,735 @@
1
+ // The canonical, full-fidelity session transcript (stigmer/stigmer#814).
2
+ //
3
+ // A session's whole conversation — thinking included, tool calls with their
4
+ // offloaded outputs resolved, sub-agent turns, timestamps — assembled ONCE
5
+ // here so every consumer (the SessionViewer export control, the CLI, an
6
+ // embedder's backend analysis job) reads the same authoritative record
7
+ // instead of re-deriving it.
8
+ //
9
+ // Canonical rules, decided for #814:
10
+ // - Fidelity beats noise suppression. Thinking and system messages are
11
+ // carried verbatim; presentation-layer cleanups (the CLI replay's
12
+ // system-error rewriting, approval-noise dropping) stay in the
13
+ // presentation layers. The transcript is raw material for improving
14
+ // agents — the judgment signal lives in exactly the parts a polished
15
+ // view hides.
16
+ // - Tool calls attach to their parent MESSAGE_AI message. Every tool call
17
+ // lives on an AI message in the data model, so this shows each call
18
+ // exactly once, in position. (The CLI replay's started_at interleaving
19
+ // is an event-stream presentation need, not a data rule.)
20
+ // - Superseded turns (edit-and-resubmit) are excluded by default, matching
21
+ // what the conversation view shows; `includeSuperseded: true` keeps them,
22
+ // marked, for the history-faithful variant.
23
+ // - An in-flight execution exports as its last-persisted snapshot and is
24
+ // marked `inProgress` — honest about what a mid-run export is.
25
+ // - Sub-agent transcripts (`status.sub_agent_executions` — task-tool
26
+ // delegations) are embedded in the parent's status and export in full. A
27
+ // `call-agent` workflow task spawns a SEPARATE child AgentExecution in
28
+ // its own session; that is a different session's transcript, not part of
29
+ // this one.
30
+ //
31
+ // Like the rest of this folder, the module is framework-free: the fetch
32
+ // entry point takes a structural client slice, so it runs in React hosts,
33
+ // Node jobs, and the CLI alike.
34
+
35
+ import { toJson, type JsonValue } from "@bufbuild/protobuf";
36
+ import { create } from "@bufbuild/protobuf";
37
+ import type {
38
+ AgentExecution,
39
+ AgentExecutionStatus,
40
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
41
+ import { AgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
42
+ import type {
43
+ AgentMessage,
44
+ ToolCall,
45
+ ToolCallOutputRef,
46
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/message_pb";
47
+ import {
48
+ MessageType,
49
+ ToolCallStatus,
50
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
51
+ import type {
52
+ GetArtifactContentResponse,
53
+ ListAgentExecutionsBySessionRequest,
54
+ AgentExecutionList,
55
+ GetArtifactContentRequest,
56
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/io_pb";
57
+ import {
58
+ GetArtifactContentRequestSchema,
59
+ ListAgentExecutionsBySessionRequestSchema,
60
+ } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/io_pb";
61
+ import type { SubAgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/subagent_pb";
62
+ import type { Session } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
63
+ import { SessionSchema } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
64
+ import { resolvedSubject } from "../session.js";
65
+ import {
66
+ execIdFromStorageKey,
67
+ isBuildFromPlanTurn,
68
+ sortChronologically,
69
+ supersededExecutionIds,
70
+ syntheticUserPrompt,
71
+ } from "./conversation-rules.js";
72
+ import { isTerminalPhase } from "./execution-phases.js";
73
+
74
+ // ============================================================================
75
+ // Model
76
+ // ============================================================================
77
+
78
+ /**
79
+ * The fetched content of one offloaded tool output
80
+ * ({@link ToolCallOutputRef}), keyed in the transcript by its storage key —
81
+ * the one identifier that is unique across executions and sub-agents.
82
+ */
83
+ export interface ResolvedToolOutput {
84
+ /** The `output_ref.storage_key` this content belongs to. */
85
+ readonly storageKey: string;
86
+ /**
87
+ * The resolved text, possibly truncated at the server's content cap
88
+ * (512 KB by default). `undefined` when the fetch failed or was skipped —
89
+ * serializers fall back to the ref's `truncated_preview`.
90
+ */
91
+ readonly content?: string;
92
+ /** `true` when the server truncated {@link content} at the byte cap. */
93
+ readonly truncated: boolean;
94
+ /** Full stored size in bytes, when known. */
95
+ readonly totalSizeBytes?: number;
96
+ /** MIME type recorded on the ref. */
97
+ readonly mimeType: string;
98
+ /** `true` for image outputs — never inlined; serializers note them. */
99
+ readonly isImage: boolean;
100
+ /** Fetch failure, when resolution was attempted and failed. */
101
+ readonly error?: string;
102
+ }
103
+
104
+ /** One execution's contribution to the conversation, with the canonical
105
+ * rules' verdicts precomputed so consumers never re-derive them. */
106
+ export interface TranscriptTurn {
107
+ /** The execution, verbatim — messages, tool calls, sub-agents, timestamps. */
108
+ readonly execution: AgentExecution;
109
+ /**
110
+ * The user prose that opened this turn, or `null` when the turn has none
111
+ * (programmatic create, `"execute"` placeholder, Build-from-plan label) —
112
+ * the shared `syntheticUserPrompt` rule.
113
+ */
114
+ readonly userPrompt: string | null;
115
+ /** `true` for a Build-from-plan turn (machine-labeled, no user prose). */
116
+ readonly isBuildFromPlan: boolean;
117
+ /** `true` when the turn was replaced by an edit-and-resubmit. Only ever
118
+ * `true` when the transcript was assembled with `includeSuperseded`. */
119
+ readonly superseded: boolean;
120
+ /** `true` when the execution had not reached a terminal phase at assembly
121
+ * time — its content is the last-persisted snapshot of a running turn. */
122
+ readonly inProgress: boolean;
123
+ }
124
+
125
+ /** The canonical whole-conversation record of one session. */
126
+ export interface SessionTranscript {
127
+ /** The session, verbatim. */
128
+ readonly session: Session;
129
+ /** The conversation's turns, in chronological (ULID) order. */
130
+ readonly turns: readonly TranscriptTurn[];
131
+ /**
132
+ * Fetched offloaded tool outputs, keyed by storage key. Empty when
133
+ * resolution was skipped; serializers then fall back to each ref's
134
+ * `truncated_preview`.
135
+ */
136
+ readonly resolvedOutputs: Readonly<Record<string, ResolvedToolOutput>>;
137
+ /** Whether superseded (edit-and-resubmit) turns were kept. */
138
+ readonly includesSuperseded: boolean;
139
+ }
140
+
141
+ // ============================================================================
142
+ // Assembly (pure)
143
+ // ============================================================================
144
+
145
+ /** Options for {@link assembleSessionTranscript}. */
146
+ export interface AssembleSessionTranscriptOptions {
147
+ /**
148
+ * Keep turns replaced by edit-and-resubmit, marked
149
+ * `superseded: true`. Defaults to `false` — the conversation as the
150
+ * viewer shows it.
151
+ */
152
+ readonly includeSuperseded?: boolean;
153
+ /** Fetched offloaded outputs to embed (see {@link resolveOffloadedOutputs}). */
154
+ readonly resolvedOutputs?: Readonly<Record<string, ResolvedToolOutput>>;
155
+ }
156
+
157
+ /**
158
+ * Assembles the canonical transcript from a session and its executions.
159
+ *
160
+ * Pure — same inputs, same transcript. Ordering, superseded filtering, and
161
+ * user-turn synthesis are the shared conversation rules from
162
+ * `conversation-rules.ts`, so this cannot drift from what the conversation
163
+ * view renders.
164
+ */
165
+ export function assembleSessionTranscript(
166
+ session: Session,
167
+ executions: readonly AgentExecution[],
168
+ options?: AssembleSessionTranscriptOptions,
169
+ ): SessionTranscript {
170
+ const includeSuperseded = options?.includeSuperseded === true;
171
+ const ordered = sortChronologically(executions);
172
+ const superseded = supersededExecutionIds(ordered);
173
+
174
+ const turns: TranscriptTurn[] = [];
175
+ for (const execution of ordered) {
176
+ const isSuperseded = superseded.has(execution.metadata?.id ?? "");
177
+ if (isSuperseded && !includeSuperseded) continue;
178
+ const phase = execution.status?.phase;
179
+ turns.push({
180
+ execution,
181
+ userPrompt: syntheticUserPrompt(execution),
182
+ isBuildFromPlan: isBuildFromPlanTurn(execution),
183
+ superseded: isSuperseded,
184
+ inProgress: phase === undefined || !isTerminalPhase(phase),
185
+ });
186
+ }
187
+
188
+ return {
189
+ session,
190
+ turns,
191
+ resolvedOutputs: options?.resolvedOutputs ?? {},
192
+ includesSuperseded: includeSuperseded,
193
+ };
194
+ }
195
+
196
+ // ============================================================================
197
+ // Fetch (the one authoritative read)
198
+ // ============================================================================
199
+
200
+ /**
201
+ * The client slice {@link fetchSessionTranscript} needs — structurally
202
+ * satisfied by the `Stigmer` client, and small enough to fake in tests and
203
+ * satisfy from any host.
204
+ */
205
+ export interface SessionTranscriptClient {
206
+ readonly session: {
207
+ get(id: string): Promise<Session>;
208
+ };
209
+ readonly agentExecution: {
210
+ listBySession(
211
+ input: ListAgentExecutionsBySessionRequest,
212
+ ): Promise<AgentExecutionList>;
213
+ getArtifactContent(
214
+ input: GetArtifactContentRequest,
215
+ ): Promise<GetArtifactContentResponse>;
216
+ };
217
+ }
218
+
219
+ /** Options for {@link fetchSessionTranscript}. */
220
+ export interface FetchSessionTranscriptOptions {
221
+ /** See {@link AssembleSessionTranscriptOptions.includeSuperseded}. */
222
+ readonly includeSuperseded?: boolean;
223
+ /**
224
+ * Fetch offloaded tool outputs (`output_ref`) so the transcript carries
225
+ * them in full instead of their truncated previews. Defaults to `true` —
226
+ * resolution is the fidelity bar of #814. Individual fetch failures never
227
+ * fail the export; the failed ref falls back to its preview, with the
228
+ * error recorded on its {@link ResolvedToolOutput}.
229
+ */
230
+ readonly resolveOutputs?: boolean;
231
+ /** Concurrent output fetches. Defaults to 4. */
232
+ readonly concurrency?: number;
233
+ }
234
+
235
+ /**
236
+ * Fetches everything a session's transcript needs and assembles it.
237
+ *
238
+ * One `listBySession` call returns the complete execution set — both server
239
+ * editions answer this RPC unpaginated today (Go: `TODO: Implement
240
+ * pagination`; Java: "not using pagination with authorized IDs approach").
241
+ * If a future server starts paginating (`total_pages > 1`), this fails with
242
+ * a descriptive error rather than silently exporting a truncated
243
+ * "full-fidelity" transcript.
244
+ */
245
+ export async function fetchSessionTranscript(
246
+ client: SessionTranscriptClient,
247
+ sessionId: string,
248
+ options?: FetchSessionTranscriptOptions,
249
+ ): Promise<SessionTranscript> {
250
+ const [session, list] = await Promise.all([
251
+ client.session.get(sessionId),
252
+ client.agentExecution.listBySession(
253
+ create(ListAgentExecutionsBySessionRequestSchema, { sessionId }),
254
+ ),
255
+ ]);
256
+
257
+ if (list.totalPages > 1) {
258
+ throw new Error(
259
+ `Session ${sessionId} has ${list.totalPages} pages of executions but ` +
260
+ "transcript export reads a single page. The server has started " +
261
+ "paginating listBySession; teach fetchSessionTranscript to walk " +
262
+ "pages before exporting, so no turns are silently dropped.",
263
+ );
264
+ }
265
+
266
+ const resolvedOutputs =
267
+ options?.resolveOutputs === false
268
+ ? {}
269
+ : await resolveOffloadedOutputs(client, list.entries, {
270
+ concurrency: options?.concurrency,
271
+ });
272
+
273
+ return assembleSessionTranscript(session, list.entries, {
274
+ includeSuperseded: options?.includeSuperseded,
275
+ resolvedOutputs,
276
+ });
277
+ }
278
+
279
+ /**
280
+ * Fetches the content behind every offloaded tool output in the given
281
+ * executions (parent and sub-agent transcripts alike), keyed by storage key.
282
+ *
283
+ * Image refs are never fetched — serializers render them as notes. The
284
+ * execution id for each fetch derives from the storage key itself
285
+ * (`execIdFromStorageKey`): sub-agent outputs are stored under the PARENT
286
+ * execution's id, and the key is the record of that.
287
+ */
288
+ export async function resolveOffloadedOutputs(
289
+ client: Pick<SessionTranscriptClient, "agentExecution">,
290
+ executions: readonly AgentExecution[],
291
+ options?: { readonly concurrency?: number },
292
+ ): Promise<Record<string, ResolvedToolOutput>> {
293
+ const refs = new Map<string, ToolCallOutputRef>();
294
+ for (const execution of executions) {
295
+ for (const toolCall of allToolCalls(execution.status)) {
296
+ const ref = toolCall.outputRef;
297
+ if (ref?.storageKey) refs.set(ref.storageKey, ref);
298
+ }
299
+ }
300
+
301
+ const resolved: Record<string, ResolvedToolOutput> = {};
302
+ const decoder = new TextDecoder("utf-8", { fatal: false });
303
+
304
+ await mapWithConcurrency(
305
+ [...refs.values()],
306
+ options?.concurrency ?? 4,
307
+ async (ref) => {
308
+ const base = {
309
+ storageKey: ref.storageKey,
310
+ mimeType: ref.mimeType,
311
+ isImage: ref.isImage,
312
+ totalSizeBytes: Number(ref.sizeBytes),
313
+ };
314
+ if (ref.isImage) {
315
+ resolved[ref.storageKey] = { ...base, truncated: false };
316
+ return;
317
+ }
318
+ const executionId = execIdFromStorageKey(ref.storageKey);
319
+ if (!executionId) {
320
+ resolved[ref.storageKey] = {
321
+ ...base,
322
+ truncated: false,
323
+ error: `unexpected storage key shape: ${ref.storageKey}`,
324
+ };
325
+ return;
326
+ }
327
+ try {
328
+ const response = await client.agentExecution.getArtifactContent(
329
+ create(GetArtifactContentRequestSchema, {
330
+ executionId,
331
+ storageKey: ref.storageKey,
332
+ }),
333
+ );
334
+ resolved[ref.storageKey] = {
335
+ ...base,
336
+ content: decoder.decode(response.content),
337
+ truncated: response.truncated,
338
+ totalSizeBytes: Number(response.totalSizeBytes),
339
+ };
340
+ } catch (e) {
341
+ resolved[ref.storageKey] = {
342
+ ...base,
343
+ truncated: false,
344
+ error: e instanceof Error ? e.message : String(e),
345
+ };
346
+ }
347
+ },
348
+ );
349
+
350
+ return resolved;
351
+ }
352
+
353
+ /** Every tool call in an execution's status — the parent transcript's plus
354
+ * each embedded sub-agent transcript's. */
355
+ function* allToolCalls(
356
+ status: AgentExecutionStatus | undefined,
357
+ ): Generator<ToolCall> {
358
+ if (!status) return;
359
+ const messageLists: readonly (readonly AgentMessage[])[] = [
360
+ status.messages,
361
+ ...status.subAgentExecutions.map((sa) => sa.messages),
362
+ ];
363
+ for (const messages of messageLists) {
364
+ for (const message of messages) {
365
+ yield* message.toolCalls;
366
+ }
367
+ }
368
+ }
369
+
370
+ /** Runs `fn` over `items` with at most `limit` in flight. */
371
+ async function mapWithConcurrency<T>(
372
+ items: readonly T[],
373
+ limit: number,
374
+ fn: (item: T) => Promise<void>,
375
+ ): Promise<void> {
376
+ let next = 0;
377
+ const workers = Array.from(
378
+ { length: Math.max(1, Math.min(limit, items.length)) },
379
+ async () => {
380
+ while (next < items.length) {
381
+ const item = items[next++];
382
+ await fn(item);
383
+ }
384
+ },
385
+ );
386
+ await Promise.all(workers);
387
+ }
388
+
389
+ // ============================================================================
390
+ // Markdown serialization
391
+ // ============================================================================
392
+
393
+ /** Options for {@link transcriptToMarkdown}. */
394
+ export interface TranscriptToMarkdownOptions {
395
+ /**
396
+ * ISO timestamp stamped in the document header as the export time.
397
+ * Omitted when not provided, keeping the serializer deterministic for
398
+ * fixture pinning.
399
+ */
400
+ readonly generatedAt?: string;
401
+ }
402
+
403
+ /**
404
+ * Renders the transcript as a self-contained Markdown document.
405
+ *
406
+ * Deterministic: same transcript, same bytes — the format is a pinned
407
+ * contract (see `__tests__/transcript.test.ts`). Roles are bold labels
408
+ * (User / Assistant / Thinking / System), thinking and system content is
409
+ * blockquoted, tool calls carry fenced args and output, and sub-agent turns
410
+ * are blockquoted sections. Fences self-size past any backtick runs in the
411
+ * content, so hostile content cannot break out of its block.
412
+ */
413
+ export function transcriptToMarkdown(
414
+ transcript: SessionTranscript,
415
+ options?: TranscriptToMarkdownOptions,
416
+ ): string {
417
+ const { session, turns, resolvedOutputs } = transcript;
418
+ const out: string[] = [];
419
+
420
+ const sessionId = session.metadata?.id ?? "";
421
+ const title = resolvedSubject(session.spec?.subject) ?? `Session ${sessionId}`;
422
+ out.push(`# ${title}`, "");
423
+ out.push(`- Session: \`${sessionId}\``);
424
+ const agentInstanceId = session.spec?.agentInstanceId;
425
+ if (agentInstanceId) out.push(`- Agent instance: \`${agentInstanceId}\``);
426
+ out.push(`- Turns: ${turns.length}`);
427
+ if (transcript.includesSuperseded) {
428
+ out.push("- Includes superseded (edited-and-resubmitted) turns");
429
+ }
430
+ if (options?.generatedAt) out.push(`- Exported: ${options.generatedAt}`);
431
+
432
+ for (let i = 0; i < turns.length; i++) {
433
+ const turn = turns[i];
434
+ out.push("", "---", "");
435
+ out.push(`## Turn ${i + 1}${turnHeaderSuffix(turn)}`);
436
+
437
+ if (turn.superseded) {
438
+ out.push("", "_Superseded by an edited resubmission._");
439
+ }
440
+ if (turn.inProgress) {
441
+ out.push("", "_In progress at export time — content is the last persisted snapshot._");
442
+ }
443
+ if (turn.isBuildFromPlan) {
444
+ out.push("", "_Build from plan._");
445
+ }
446
+
447
+ if (turn.userPrompt !== null) {
448
+ out.push("", "**User**", "", turn.userPrompt);
449
+ }
450
+
451
+ renderMessages(
452
+ out,
453
+ turn.execution.status?.messages ?? [],
454
+ turn.execution.status?.subAgentExecutions ?? [],
455
+ resolvedOutputs,
456
+ turn.userPrompt,
457
+ );
458
+
459
+ const error = turn.execution.status?.error;
460
+ if (error) {
461
+ out.push("", "**Execution error**", "", blockquote(error));
462
+ }
463
+ }
464
+
465
+ out.push("");
466
+ return out.join("\n");
467
+ }
468
+
469
+ function turnHeaderSuffix(turn: TranscriptTurn): string {
470
+ const startedAt = turn.execution.status?.startedAt;
471
+ return startedAt ? ` · ${startedAt}` : "";
472
+ }
473
+
474
+ /** Renders one message list — the parent turn's or a sub-agent's. */
475
+ function renderMessages(
476
+ out: string[],
477
+ messages: readonly AgentMessage[],
478
+ subAgents: readonly SubAgentExecution[],
479
+ resolvedOutputs: Readonly<Record<string, ResolvedToolOutput>>,
480
+ userPrompt: string | null,
481
+ ): void {
482
+ for (const message of messages) {
483
+ switch (message.type) {
484
+ case MessageType.MESSAGE_TOOL:
485
+ // Tool calls render from their parent MESSAGE_AI's tool_calls (the
486
+ // canonical attach rule); the MESSAGE_TOOL echo would double them.
487
+ continue;
488
+ case MessageType.MESSAGE_HUMAN: {
489
+ // The turn's opening prompt is already rendered from spec.message;
490
+ // skip its status.messages echo. Any OTHER human message is kept.
491
+ if (userPrompt !== null && message.content === userPrompt) continue;
492
+ out.push("", "**User**", "", message.content);
493
+ continue;
494
+ }
495
+ case MessageType.MESSAGE_THINKING:
496
+ out.push("", "**Thinking**", "", blockquote(message.content));
497
+ continue;
498
+ case MessageType.MESSAGE_SYSTEM:
499
+ out.push("", "**System**", "", blockquote(message.content));
500
+ continue;
501
+ case MessageType.MESSAGE_AI: {
502
+ if (message.content.trim()) {
503
+ out.push("", "**Assistant**", "", message.content);
504
+ }
505
+ for (const toolCall of message.toolCalls) {
506
+ if (toolCall.name === "task") {
507
+ const subAgent = subAgents.find((sa) => sa.id === toolCall.id);
508
+ if (subAgent) {
509
+ renderSubAgent(out, subAgent, resolvedOutputs);
510
+ continue;
511
+ }
512
+ }
513
+ renderToolCall(out, toolCall, resolvedOutputs);
514
+ }
515
+ continue;
516
+ }
517
+ default:
518
+ // Unknown future message type: carry the content rather than drop it.
519
+ if (message.content) out.push("", blockquote(message.content));
520
+ }
521
+ }
522
+ }
523
+
524
+ function renderToolCall(
525
+ out: string[],
526
+ toolCall: ToolCall,
527
+ resolvedOutputs: Readonly<Record<string, ResolvedToolOutput>>,
528
+ ): void {
529
+ out.push("", `**Tool — \`${toolCall.name}\`**${toolCallSuffix(toolCall)}`);
530
+
531
+ // ToolCall.args is a google.protobuf.Struct, which protobuf-es represents
532
+ // as a plain JsonObject — safe to stringify directly (no bigint fields).
533
+ const args = toolCall.args
534
+ ? JSON.stringify(toolCall.args, null, 2)
535
+ : undefined;
536
+ if (args && args !== "{}") {
537
+ out.push("", fenced(args, "json"));
538
+ }
539
+
540
+ const output = toolOutputText(toolCall, resolvedOutputs);
541
+ if (output.note) out.push("", `_${output.note}_`);
542
+ if (output.text) out.push("", fenced(output.text));
543
+
544
+ if (toolCall.error) {
545
+ out.push("", "Error:", "", fenced(toolCall.error));
546
+ }
547
+ }
548
+
549
+ /** The output text and honesty note for a tool call, applying the
550
+ * offload-resolution fallbacks. */
551
+ function toolOutputText(
552
+ toolCall: ToolCall,
553
+ resolvedOutputs: Readonly<Record<string, ResolvedToolOutput>>,
554
+ ): { text?: string; note?: string } {
555
+ const ref = toolCall.outputRef;
556
+ if (!ref?.storageKey) {
557
+ return toolCall.result ? { text: toolCall.result } : {};
558
+ }
559
+ if (ref.isImage) {
560
+ return {
561
+ note: `Image output (${ref.mimeType || "image"}, ${Number(ref.sizeBytes)} bytes) — not inlined.`,
562
+ };
563
+ }
564
+ const resolved = resolvedOutputs[ref.storageKey];
565
+ if (resolved?.content !== undefined) {
566
+ return {
567
+ text: resolved.content,
568
+ note: resolved.truncated
569
+ ? `Output truncated at ${resolved.content.length} of ${resolved.totalSizeBytes} bytes (server content cap).`
570
+ : undefined,
571
+ };
572
+ }
573
+ return {
574
+ text: ref.truncatedPreview || toolCall.result || undefined,
575
+ note: resolved?.error
576
+ ? `Offloaded output unavailable (${resolved.error}) — showing preview.`
577
+ : "Offloaded output not resolved — showing preview.",
578
+ };
579
+ }
580
+
581
+ function toolCallSuffix(toolCall: ToolCall): string {
582
+ const parts: string[] = [];
583
+ const status = toolCallStatusLabel(toolCall.status);
584
+ if (status) parts.push(status);
585
+ const duration = formatDuration(toolCall.startedAt, toolCall.completedAt);
586
+ if (duration) parts.push(duration);
587
+ return parts.length > 0 ? ` (${parts.join(" · ")})` : "";
588
+ }
589
+
590
+ function toolCallStatusLabel(status: ToolCallStatus): string | null {
591
+ switch (status) {
592
+ case ToolCallStatus.TOOL_CALL_COMPLETED:
593
+ return "completed";
594
+ case ToolCallStatus.TOOL_CALL_FAILED:
595
+ return "failed";
596
+ case ToolCallStatus.TOOL_CALL_SKIPPED:
597
+ return "skipped";
598
+ case ToolCallStatus.TOOL_CALL_INTERRUPTED:
599
+ return "interrupted";
600
+ case ToolCallStatus.TOOL_CALL_RUNNING:
601
+ return "running";
602
+ case ToolCallStatus.TOOL_CALL_WAITING_APPROVAL:
603
+ return "waiting for approval";
604
+ case ToolCallStatus.TOOL_CALL_PENDING:
605
+ return "pending";
606
+ default:
607
+ return null;
608
+ }
609
+ }
610
+
611
+ function renderSubAgent(
612
+ out: string[],
613
+ subAgent: SubAgentExecution,
614
+ resolvedOutputs: Readonly<Record<string, ResolvedToolOutput>>,
615
+ ): void {
616
+ const nested: string[] = [];
617
+ const title = subAgent.subject || subAgent.name || "sub-agent";
618
+ nested.push(`**Sub-agent — ${title}**${subAgentSuffix(subAgent)}`);
619
+ if (subAgent.input) {
620
+ nested.push("", "Input:", "", subAgent.input);
621
+ }
622
+ renderMessages(nested, subAgent.messages, [], resolvedOutputs, null);
623
+ if (subAgent.output) {
624
+ nested.push("", "Output:", "", subAgent.output);
625
+ }
626
+ if (subAgent.error) {
627
+ nested.push("", "Error:", "", fenced(subAgent.error));
628
+ }
629
+ out.push("", blockquote(nested.join("\n")));
630
+ }
631
+
632
+ function subAgentSuffix(subAgent: SubAgentExecution): string {
633
+ const duration = formatDuration(subAgent.startedAt, subAgent.completedAt);
634
+ return duration ? ` (${duration})` : "";
635
+ }
636
+
637
+ /** `"3.2s"` when both ISO timestamps parse and are ordered, else `null`. */
638
+ function formatDuration(
639
+ startedAt: string | undefined,
640
+ completedAt: string | undefined,
641
+ ): string | null {
642
+ if (!startedAt || !completedAt) return null;
643
+ const start = Date.parse(startedAt);
644
+ const end = Date.parse(completedAt);
645
+ if (Number.isNaN(start) || Number.isNaN(end) || end < start) return null;
646
+ const ms = end - start;
647
+ if (ms < 1000) return `${ms}ms`;
648
+ return `${(ms / 1000).toFixed(1)}s`;
649
+ }
650
+
651
+ /** Prefixes every line with `> ` (blockquote), including blank lines. */
652
+ function blockquote(text: string): string {
653
+ return text
654
+ .split("\n")
655
+ .map((line) => (line.length > 0 ? `> ${line}` : ">"))
656
+ .join("\n");
657
+ }
658
+
659
+ /**
660
+ * Wraps content in a code fence sized past its longest backtick run, so
661
+ * content containing ``` can never terminate the block early.
662
+ */
663
+ function fenced(content: string, language = ""): string {
664
+ let longest = 0;
665
+ for (const match of content.matchAll(/`+/g)) {
666
+ if (match[0].length > longest) longest = match[0].length;
667
+ }
668
+ const fence = "`".repeat(Math.max(3, longest + 1));
669
+ return `${fence}${language}\n${content}\n${fence}`;
670
+ }
671
+
672
+ // ============================================================================
673
+ // JSON serialization
674
+ // ============================================================================
675
+
676
+ /**
677
+ * Renders the transcript as a plain JSON value for programmatic analysis.
678
+ *
679
+ * Proto parts serialize through `toJson` with `useProtoFieldName: true` —
680
+ * the platform-wide protojson parity contract (snake_case fields, int64 as
681
+ * strings, `fromJson` round-trippable). Never `JSON.stringify` a raw proto
682
+ * object here: int64 fields are `bigint` and would throw.
683
+ *
684
+ * One deliberate omission: `status.callback_token` is stripped. It is an
685
+ * internal Temporal task token (runtime plumbing for workflow-triggered
686
+ * executions), not conversation material, and has no place in a document
687
+ * users share and paste around.
688
+ */
689
+ export function transcriptToJson(transcript: SessionTranscript): JsonValue {
690
+ const turns: JsonValue[] = transcript.turns.map((turn) => {
691
+ const execution = toJson(AgentExecutionSchema, turn.execution, {
692
+ useProtoFieldName: true,
693
+ });
694
+ if (
695
+ execution !== null &&
696
+ typeof execution === "object" &&
697
+ !Array.isArray(execution)
698
+ ) {
699
+ const status = execution["status"];
700
+ if (status !== null && typeof status === "object" && !Array.isArray(status)) {
701
+ delete (status as Record<string, unknown>)["callback_token"];
702
+ }
703
+ }
704
+ return {
705
+ user_prompt: turn.userPrompt,
706
+ build_from_plan: turn.isBuildFromPlan || undefined,
707
+ superseded: turn.superseded || undefined,
708
+ in_progress: turn.inProgress || undefined,
709
+ execution,
710
+ } as unknown as JsonValue;
711
+ });
712
+
713
+ const resolvedOutputs: Record<string, JsonValue> = {};
714
+ for (const [key, output] of Object.entries(transcript.resolvedOutputs)) {
715
+ resolvedOutputs[key] = {
716
+ storage_key: output.storageKey,
717
+ content: output.content,
718
+ truncated: output.truncated || undefined,
719
+ total_size_bytes: output.totalSizeBytes,
720
+ mime_type: output.mimeType,
721
+ is_image: output.isImage || undefined,
722
+ error: output.error,
723
+ } as unknown as JsonValue;
724
+ }
725
+
726
+ return {
727
+ format: "stigmer.ai/session-transcript/v1",
728
+ session: toJson(SessionSchema, transcript.session, {
729
+ useProtoFieldName: true,
730
+ }),
731
+ includes_superseded: transcript.includesSuperseded,
732
+ turns,
733
+ resolved_outputs: resolvedOutputs,
734
+ };
735
+ }