baa-conductor


baa-conductor / packages / artifact-db / src
im_wower  ·  2026-03-28

static-gen.ts

  1import {
  2  ARTIFACT_PUBLIC_PATH_SEGMENT,
  3  type ArtifactScope,
  4  type ArtifactTextFile,
  5  type ExecutionRecord,
  6  type MessageRecord,
  7  type SessionIndexEntry,
  8  type SessionRecord,
  9  type SessionTimelineEntry
 10} from "./types.js";
 11
 12const JSON_CONTENT_TYPE = "application/json; charset=utf-8";
 13const TEXT_CONTENT_TYPE = "text/plain; charset=utf-8";
 14
 15interface ArtifactRenderConfig {
 16  publicBaseUrl: string | null;
 17}
 18
 19export function buildArtifactRelativePath(scope: ArtifactScope, fileName: string): string {
 20  return `${scope}/${fileName}`;
 21}
 22
 23export function buildArtifactPublicUrl(
 24  publicBaseUrl: string | null,
 25  relativePath: string | null
 26): string | null {
 27  if (publicBaseUrl == null || relativePath == null) {
 28    return null;
 29  }
 30
 31  return `${publicBaseUrl}/${ARTIFACT_PUBLIC_PATH_SEGMENT}/${relativePath}`;
 32}
 33
 34export function getArtifactContentType(path: string): string {
 35  return path.endsWith(".json") ? JSON_CONTENT_TYPE : TEXT_CONTENT_TYPE;
 36}
 37
 38export function buildMessageArtifactFiles(
 39  record: MessageRecord,
 40  config: ArtifactRenderConfig
 41): ArtifactTextFile[] {
 42  const txtPath = record.staticPath;
 43  const jsonPath = replaceArtifactExtension(record.staticPath, ".json");
 44
 45  return [
 46    {
 47      content: renderMessageText(record),
 48      kind: "txt",
 49      relativePath: txtPath
 50    },
 51    {
 52      content: renderJson({
 53        artifact_url: buildArtifactPublicUrl(config.publicBaseUrl, txtPath),
 54        conversation_id: record.conversationId,
 55        created_at: formatTimestamp(record.createdAt),
 56        id: record.id,
 57        kind: "message",
 58        observed_at: formatTimestamp(record.observedAt),
 59        organization_id: record.organizationId,
 60        page_title: record.pageTitle,
 61        page_url: record.pageUrl,
 62        platform: record.platform,
 63        raw_text: record.rawText,
 64        role: record.role,
 65        static_path: txtPath,
 66        summary: record.summary
 67      }),
 68      kind: "json",
 69      relativePath: jsonPath
 70    }
 71  ];
 72}
 73
 74export function buildExecutionArtifactFiles(
 75  record: ExecutionRecord,
 76  config: ArtifactRenderConfig,
 77  messageStaticPath: string | null
 78): ArtifactTextFile[] {
 79  const txtPath = record.staticPath;
 80  const jsonPath = replaceArtifactExtension(record.staticPath, ".json");
 81
 82  return [
 83    {
 84      content: renderExecutionText(record, config, messageStaticPath),
 85      kind: "txt",
 86      relativePath: txtPath
 87    },
 88    {
 89      content: renderJson({
 90        artifact_url: buildArtifactPublicUrl(config.publicBaseUrl, txtPath),
 91        executed_at: formatTimestamp(record.executedAt),
 92        http_status: record.httpStatus,
 93        id: record.instructionId,
 94        kind: "execution",
 95        message_id: record.messageId,
 96        message_url: buildArtifactPublicUrl(config.publicBaseUrl, messageStaticPath),
 97        params: parseJsonText(record.params),
 98        params_kind: record.paramsKind,
 99        result: {
100          data: parseJsonText(record.resultData),
101          error: record.resultError,
102          ok: record.resultOk
103        },
104        static_path: txtPath,
105        status: record.resultOk ? "ok" : "error",
106        summary: record.resultSummary,
107        target: record.target,
108        tool: record.tool
109      }),
110      kind: "json",
111      relativePath: jsonPath
112    }
113  ];
114}
115
116export function buildSessionArtifactFiles(
117  record: SessionRecord,
118  timeline: SessionTimelineEntry[],
119  config: ArtifactRenderConfig
120): ArtifactTextFile[] {
121  const txtPath = record.staticPath;
122  const jsonPath = replaceArtifactExtension(record.staticPath, ".json");
123
124  return [
125    {
126      content: renderSessionText(record, timeline, config),
127      kind: "txt",
128      relativePath: txtPath
129    },
130    {
131      content: renderJson({
132        artifact_url: buildArtifactPublicUrl(config.publicBaseUrl, txtPath),
133        conversation_id: record.conversationId,
134        created_at: formatTimestamp(record.createdAt),
135        execution_count: record.executionCount,
136        id: record.id,
137        kind: "session",
138        last_activity_at: formatTimestamp(record.lastActivityAt),
139        message_count: record.messageCount,
140        platform: record.platform,
141        started_at: formatTimestamp(record.startedAt),
142        static_path: txtPath,
143        summary: record.summary,
144        timeline: timeline.map((entry) => serializeTimelineEntry(entry, config.publicBaseUrl))
145      }),
146      kind: "json",
147      relativePath: jsonPath
148    }
149  ];
150}
151
152export function buildSessionIndexArtifactFiles(
153  entries: SessionIndexEntry[],
154  generatedAt: number,
155  config: ArtifactRenderConfig
156): ArtifactTextFile[] {
157  const txtPath = buildArtifactRelativePath("session", "latest.txt");
158  const jsonPath = buildArtifactRelativePath("session", "latest.json");
159
160  return [
161    {
162      content: renderSessionIndexText(entries, generatedAt, config),
163      kind: "txt",
164      relativePath: txtPath
165    },
166    {
167      content: renderJson({
168        count: entries.length,
169        generated_at: formatTimestamp(generatedAt),
170        kind: "session_index",
171        sessions: entries.map((entry) => ({
172          conversation_id: entry.conversationId,
173          execution_count: entry.executionCount,
174          id: entry.id,
175          last_activity_at: formatTimestamp(entry.lastActivityAt),
176          latest_message_url: buildArtifactPublicUrl(config.publicBaseUrl, entry.latestMessageStaticPath),
177          message_count: entry.messageCount,
178          platform: entry.platform,
179          session_url: buildArtifactPublicUrl(config.publicBaseUrl, entry.staticPath),
180          summary: entry.summary
181        }))
182      }),
183      kind: "json",
184      relativePath: jsonPath
185    }
186  ];
187}
188
189function renderMessageText(record: MessageRecord): string {
190  return [
191    renderFrontmatter({
192      conversation_id: record.conversationId,
193      created_at: formatTimestamp(record.createdAt),
194      id: record.id,
195      kind: "message",
196      observed_at: formatTimestamp(record.observedAt),
197      organization_id: record.organizationId,
198      page_title: record.pageTitle,
199      page_url: record.pageUrl,
200      platform: record.platform,
201      role: record.role,
202      summary: record.summary
203    }),
204    "",
205    "---",
206    "",
207    record.rawText
208  ].join("\n");
209}
210
211function renderExecutionText(
212  record: ExecutionRecord,
213  config: ArtifactRenderConfig,
214  messageStaticPath: string | null
215): string {
216  const bodySections: string[] = [];
217  const formattedParams = formatStructuredValue(record.params);
218  const formattedResultData = formatStructuredValue(record.resultData);
219
220  if (formattedParams) {
221    bodySections.push(`params:\n${formattedParams}`);
222  }
223
224  if (record.resultSummary) {
225    bodySections.push(`summary:\n${record.resultSummary}`);
226  }
227
228  if (formattedResultData) {
229    bodySections.push(`result:\n${formattedResultData}`);
230  }
231
232  if (record.resultError) {
233    bodySections.push(`error:\n${record.resultError}`);
234  }
235
236  if (bodySections.length === 0) {
237    bodySections.push("(empty)");
238  }
239
240  return [
241    renderFrontmatter({
242      executed_at: formatTimestamp(record.executedAt),
243      http_status: record.httpStatus,
244      id: record.instructionId,
245      kind: "execution",
246      message_id: record.messageId,
247      message_url: buildArtifactPublicUrl(config.publicBaseUrl, messageStaticPath),
248      params_kind: record.paramsKind,
249      status: record.resultOk ? "ok" : "error",
250      target: record.target,
251      tool: record.tool
252    }),
253    "",
254    "---",
255    "",
256    bodySections.join("\n\n")
257  ].join("\n");
258}
259
260function renderSessionText(
261  record: SessionRecord,
262  timeline: SessionTimelineEntry[],
263  config: ArtifactRenderConfig
264): string {
265  const sections = [
266    renderFrontmatter({
267      conversation_id: record.conversationId,
268      created_at: formatTimestamp(record.createdAt),
269      execution_count: record.executionCount,
270      id: record.id,
271      kind: "session",
272      last_activity_at: formatTimestamp(record.lastActivityAt),
273      message_count: record.messageCount,
274      platform: record.platform,
275      started_at: formatTimestamp(record.startedAt),
276      summary: record.summary
277    }),
278    "",
279    "---",
280    ""
281  ];
282
283  if (record.summary) {
284    sections.push(record.summary, "");
285  }
286
287  for (const entry of timeline) {
288    sections.push(renderTimelineEntry(entry, config.publicBaseUrl), "");
289  }
290
291  return sections.join("\n").trimEnd();
292}
293
294function renderSessionIndexText(
295  entries: SessionIndexEntry[],
296  generatedAt: number,
297  config: ArtifactRenderConfig
298): string {
299  const sections = [
300    renderFrontmatter({
301      count: entries.length,
302      generated_at: formatTimestamp(generatedAt),
303      kind: "session_index"
304    }),
305    "",
306    "---",
307    ""
308  ];
309
310  for (const entry of entries) {
311    sections.push(
312      `## [${formatCompactTimestamp(entry.lastActivityAt)}] ${entry.platform} ${entry.conversationId ?? "no-conversation"}`,
313      `messages: ${entry.messageCount}, executions: ${entry.executionCount}, last_activity: ${formatTimeOnly(entry.lastActivityAt)}`,
314      `latest_message: ${buildArtifactPublicUrl(config.publicBaseUrl, entry.latestMessageStaticPath) ?? "(none)"}`,
315      `session: ${buildArtifactPublicUrl(config.publicBaseUrl, entry.staticPath) ?? "(none)"}`,
316      entry.summary ? `summary: ${entry.summary}` : "summary: (none)",
317      ""
318    );
319  }
320
321  return sections.join("\n").trimEnd();
322}
323
324function renderTimelineEntry(entry: SessionTimelineEntry, publicBaseUrl: string | null): string {
325  if (entry.kind === "message") {
326    return [
327      `### message ${entry.id}`,
328      `role: ${entry.role}`,
329      `observed_at: ${formatTimestamp(entry.observedAt)}`,
330      `url: ${buildArtifactPublicUrl(publicBaseUrl, entry.staticPath) ?? "(none)"}`,
331      `summary: ${entry.summary ?? "(none)"}`
332    ].join("\n");
333  }
334
335  return [
336    `### execution ${entry.instructionId}`,
337    `target: ${entry.target}`,
338    `tool: ${entry.tool}`,
339    `executed_at: ${formatTimestamp(entry.executedAt)}`,
340    `status: ${entry.resultOk ? "ok" : "error"}`,
341    `url: ${buildArtifactPublicUrl(publicBaseUrl, entry.staticPath) ?? "(none)"}`,
342    `summary: ${entry.resultSummary ?? "(none)"}`
343  ].join("\n");
344}
345
346function serializeTimelineEntry(entry: SessionTimelineEntry, publicBaseUrl: string | null): Record<string, unknown> {
347  if (entry.kind === "message") {
348    return {
349      artifact_url: buildArtifactPublicUrl(publicBaseUrl, entry.staticPath),
350      id: entry.id,
351      kind: entry.kind,
352      observed_at: formatTimestamp(entry.observedAt),
353      page_title: entry.pageTitle,
354      page_url: entry.pageUrl,
355      role: entry.role,
356      summary: entry.summary
357    };
358  }
359
360  return {
361    artifact_url: buildArtifactPublicUrl(publicBaseUrl, entry.staticPath),
362    executed_at: formatTimestamp(entry.executedAt),
363    id: entry.instructionId,
364    kind: entry.kind,
365    message_id: entry.messageId,
366    status: entry.resultOk ? "ok" : "error",
367    summary: entry.resultSummary,
368    target: entry.target,
369    tool: entry.tool
370  };
371}
372
373function renderFrontmatter(values: Record<string, string | number | null>): string {
374  const lines: string[] = [];
375
376  for (const [key, value] of Object.entries(values)) {
377    if (value == null) {
378      continue;
379    }
380
381    lines.push(`${key}: ${value}`);
382  }
383
384  return lines.join("\n");
385}
386
387function renderJson(payload: Record<string, unknown>): string {
388  return `${JSON.stringify(payload, null, 2)}\n`;
389}
390
391function replaceArtifactExtension(path: string, extension: ".json" | ".txt"): string {
392  return path.replace(/\.[^.]+$/u, extension);
393}
394
395function parseJsonText(value: string | null): unknown {
396  if (value == null) {
397    return null;
398  }
399
400  const normalized = value.trim();
401
402  if (normalized === "") {
403    return "";
404  }
405
406  try {
407    return JSON.parse(normalized) as unknown;
408  } catch {
409    return value;
410  }
411}
412
413function formatStructuredValue(value: string | null): string {
414  const parsed = parseJsonText(value);
415
416  if (parsed == null) {
417    return "";
418  }
419
420  if (typeof parsed === "string") {
421    return parsed;
422  }
423
424  return JSON.stringify(parsed, null, 2);
425}
426
427function formatTimestamp(value: number): string {
428  const date = new Date(value);
429  const offsetMinutes = -date.getTimezoneOffset();
430  const absoluteOffsetMinutes = Math.abs(offsetMinutes);
431  const sign = offsetMinutes >= 0 ? "+" : "-";
432  const offsetHours = String(Math.floor(absoluteOffsetMinutes / 60)).padStart(2, "0");
433  const offsetRemainder = String(absoluteOffsetMinutes % 60).padStart(2, "0");
434
435  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}${sign}${offsetHours}:${offsetRemainder}`;
436}
437
438function formatCompactTimestamp(value: number): string {
439  const date = new Date(value);
440  return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
441}
442
443function formatTimeOnly(value: number): string {
444  const date = new Date(value);
445  return `${pad(date.getHours())}:${pad(date.getMinutes())}`;
446}
447
448function pad(value: number): string {
449  return String(value).padStart(2, "0");
450}