From 5abcad51c81daa4ba4b570cccde06eecd8a437f1 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 21 Jul 2026 00:50:41 -0700 Subject: [PATCH] refactor(media): introduce neutral MediaFact owner with identity-preserving adapters (#112197) * refactor(media): introduce neutral MediaFact owner with identity-preserving adapters Media-facts program PR 1 (audit-frozen plan): canonical MediaFact ownership and normalization/projection helpers in a neutral core media module; legacy channel/plugin/agent payload builders become adapters that project exactly their declared input fields (structural richer inputs cannot leak url/kind into historically path/contentType-only outputs); persisted transcript input keeps accepting arbitrary stored kind strings, narrowing only during runtime-fact normalization. * fix(media): restore SDK and dead-code gates --- .../.generated/plugin-sdk-api-baseline.sha256 | 2 +- src/auto-reply/reply/history.types.ts | 13 +- src/channels/inbound-event/media.test.ts | 127 +++++++++++++----- src/channels/inbound-event/media.ts | 83 ++---------- src/channels/plugins/media-payload.ts | 49 ++----- src/channels/turn/types.ts | 11 +- src/media/media-facts.ts | 104 ++++++++++++++ src/plugin-sdk/agent-media-payload.ts | 25 +--- src/sessions/user-turn-transcript.test.ts | 16 +++ src/sessions/user-turn-transcript.types.ts | 6 +- 10 files changed, 255 insertions(+), 181 deletions(-) create mode 100644 src/media/media-facts.ts diff --git a/docs/.generated/plugin-sdk-api-baseline.sha256 b/docs/.generated/plugin-sdk-api-baseline.sha256 index cca2d42b40c7..0a4525e6533e 100644 --- a/docs/.generated/plugin-sdk-api-baseline.sha256 +++ b/docs/.generated/plugin-sdk-api-baseline.sha256 @@ -27,7 +27,7 @@ ad60ccc4fe9084d47f0477e02d9296bacad32f26d7456e2a84be8d25a53a25c2 module/boolean a6bc51efd3ce23e65432c9a6790670e2a0acb96ae7263fedbaed52ea353228be module/channel-core fbf353eb38ae68d8ded3f2a60b432c7bb2c245d2ec7e7c9f53c6da19a0db0938 module/channel-entry-contract 982f29a18e07228e3da82cae67d06ff38249592a29c2fd28f01f0d2016ff80d9 module/channel-feedback -7de503d739b127b23af2c7f13eb6064f69dfb9f29d1ba7bd8d98c524a3b7257d module/channel-inbound +d1df68d6c5773da6113f8b237dc75316ec1fb878c807cb00d86070aadadccabf module/channel-inbound 2940039d5ebdb16c1d745914390db01c0f5f1e9cff33f95ea36c9e77d97c2bae module/channel-inbound-debounce 91425ad75ddb58a3796f7bc88570cb2c81b3db0dd6521a7ce0e55f9148880a47 module/channel-ingress-runtime c97dd36cdf8f83c2893c33e9430a93cd131a03d855725783ca5b545de0cf84f8 module/channel-lifecycle diff --git a/src/auto-reply/reply/history.types.ts b/src/auto-reply/reply/history.types.ts index 46578fe0be0b..878ff0dd0cd2 100644 --- a/src/auto-reply/reply/history.types.ts +++ b/src/auto-reply/reply/history.types.ts @@ -1,3 +1,5 @@ +import type { MediaFact } from "../../media/media-facts.js"; + /** Normalized history message used when building reply context. */ export type HistoryEntry = { sender: string; @@ -8,10 +10,7 @@ export type HistoryEntry = { }; /** Media metadata attached to a normalized history message. */ -export type HistoryMediaEntry = { - path?: string; - url?: string; - contentType?: string; - kind?: import("@openclaw/media-core/constants").MediaKind; - messageId?: string; -}; +export type HistoryMediaEntry = Pick< + MediaFact, + "contentType" | "kind" | "messageId" | "path" | "url" +>; diff --git a/src/channels/inbound-event/media.test.ts b/src/channels/inbound-event/media.test.ts index c4a1d077074b..82be5344d562 100644 --- a/src/channels/inbound-event/media.test.ts +++ b/src/channels/inbound-event/media.test.ts @@ -1,12 +1,16 @@ // Inbound event media tests cover channel media attachment normalization. import { describe, expect, it } from "vitest"; import { normalizeAttachments } from "../../media-understanding/attachments.normalize.js"; +import { normalizeMediaFacts, projectMediaFacts } from "../../media/media-facts.js"; +import { buildAgentMediaPayload } from "../../plugin-sdk/agent-media-payload.js"; +import { buildMediaPayload } from "../plugins/media-payload.js"; import { buildChannelInboundMediaPayload, formatMediaPlaceholderText, formatInboundMediaUnavailableText, toHistoryMediaEntries, toInboundMediaFacts, + type ChannelInboundMediaInput, } from "./media.js"; describe("channel inbound media facts", () => { @@ -61,27 +65,20 @@ describe("channel inbound media facts", () => { }); it("normalizes provider media into inbound media facts", () => { - expect( - toInboundMediaFacts( - [ - { - path: " /tmp/image.png ", - contentType: " image/png ", - messageId: " ", - }, - { - url: "https://example.test/audio.mp3", - contentType: "audio/mpeg", - kind: "audio", - }, - ], - { - kind: "image", - messageId: "msg-1", - transcribed: (_media, index) => index === 1, - }, - ), - ).toEqual([ + const input = [ + { path: " /tmp/image.png ", contentType: " image/png ", messageId: " " }, + { + url: "https://example.test/audio.mp3", + contentType: "audio/mpeg", + kind: "audio" as const, + }, + ]; + const defaults = { + kind: "image" as const, + messageId: "msg-1", + transcribed: (_media: ChannelInboundMediaInput, index: number): boolean => index === 1, + }; + const expected = [ { path: "/tmp/image.png", url: undefined, @@ -98,21 +95,35 @@ describe("channel inbound media facts", () => { transcribed: true, messageId: "msg-1", }, + ]; + expect(normalizeMediaFacts(input, defaults)).toEqual(expected); + expect(toInboundMediaFacts(input, defaults)).toEqual(expected); + expect( + normalizeMediaFacts([{ path: " image.png ", workspaceDir: " /tmp/workspace " }]), + ).toEqual([ + { + path: "image.png", + url: undefined, + contentType: undefined, + kind: undefined, + transcribed: false, + messageId: undefined, + workspaceDir: "/tmp/workspace", + }, ]); }); it("builds legacy media payload fields from inbound media facts", () => { - expect( - buildChannelInboundMediaPayload([ - { path: "/tmp/image.png", contentType: "image/png", kind: "image" }, - { - url: "https://example.test/audio.mp3", - contentType: "audio/mpeg", - kind: "audio", - transcribed: true, - }, - ]), - ).toEqual({ + const media = [ + { path: "/tmp/image.png", contentType: "image/png", kind: "image" as const }, + { + url: "https://example.test/audio.mp3", + contentType: "audio/mpeg", + kind: "audio" as const, + transcribed: true, + }, + ]; + const expected = { MediaPath: "/tmp/image.png", MediaUrl: "/tmp/image.png", MediaType: "image/png", @@ -120,7 +131,9 @@ describe("channel inbound media facts", () => { MediaUrls: ["/tmp/image.png", "https://example.test/audio.mp3"], MediaTypes: ["image/png", "audio/mpeg"], MediaTranscribedIndexes: [1], - }); + }; + expect(projectMediaFacts(media)).toEqual(expected); + expect(buildChannelInboundMediaPayload(media)).toEqual(expected); }); it("keeps legacy media arrays index-aligned for mixed path and URL media", () => { @@ -138,6 +151,54 @@ describe("channel inbound media facts", () => { ]); }); + it("keeps compact and cardinality-preserving adapter projections byte-identical", () => { + const media = [{ path: "/tmp/image.png", contentType: "image/png" }, { path: "/tmp/file.bin" }]; + const compact = { + MediaPath: "/tmp/image.png", + MediaUrl: "/tmp/image.png", + MediaType: "image/png", + MediaPaths: ["/tmp/image.png", "/tmp/file.bin"], + MediaUrls: ["/tmp/image.png", "/tmp/file.bin"], + MediaTypes: ["image/png"], + }; + expect(projectMediaFacts(media, "compact")).toEqual(compact); + expect(buildAgentMediaPayload(media)).toEqual(compact); + expect(buildMediaPayload(media)).toEqual(compact); + + const aligned = { ...compact, MediaTypes: ["image/png", ""] }; + expect(projectMediaFacts(media, "aligned")).toEqual(aligned); + expect(buildMediaPayload(media, { preserveMediaTypeCardinality: true })).toEqual(aligned); + }); + + it("keeps richer fact fields out of legacy outbound payloads", () => { + const richerMedia = [ + { + path: "/tmp/voice-note.ogg", + url: "https://example.test/voice-note.ogg", + kind: "audio" as const, + }, + ]; + const compact = { + MediaPath: "/tmp/voice-note.ogg", + MediaUrl: "/tmp/voice-note.ogg", + MediaType: undefined, + MediaPaths: ["/tmp/voice-note.ogg"], + MediaUrls: ["/tmp/voice-note.ogg"], + MediaTypes: undefined, + }; + expect(projectMediaFacts(richerMedia, "compact")).toEqual(compact); + expect(buildAgentMediaPayload(richerMedia)).toEqual(compact); + expect(buildMediaPayload(richerMedia)).toEqual(compact); + expect(projectMediaFacts(richerMedia, "aligned")).toEqual({ + ...compact, + MediaTypes: [""], + }); + expect(buildMediaPayload(richerMedia, { preserveMediaTypeCardinality: true })).toEqual({ + ...compact, + MediaTypes: [""], + }); + }); + it("maps inbound media facts into history media entries", () => { expect( toHistoryMediaEntries([{ path: "/tmp/image.png", contentType: "image/png" }], { diff --git a/src/channels/inbound-event/media.ts b/src/channels/inbound-event/media.ts index 4fd2b3199c9e..27f6888dab79 100644 --- a/src/channels/inbound-event/media.ts +++ b/src/channels/inbound-event/media.ts @@ -1,16 +1,14 @@ import { kindFromMime, mimeTypeFromFilePath } from "@openclaw/media-core/mime"; -/** - * Channel inbound media normalization. - * - * Converts plugin attachment metadata into aligned prompt/context media payload fields. - */ -import { normalizeOptionalString as normalizeString } from "@openclaw/normalization-core/string-coerce"; +/** Channel inbound media normalization and compatibility projection. */ import type { HistoryMediaEntry } from "../../auto-reply/reply/history.types.js"; +import { + normalizeMediaFacts, + projectMediaFacts, + type MediaFactLegacyProjection, +} from "../../media/media-facts.js"; import type { InboundMediaFacts } from "../turn/types.js"; -/** - * Attachment metadata accepted from channel plugins before core normalization. - */ +/** Attachment metadata accepted from channel plugins before core normalization. */ export type ChannelInboundMediaInput = { path?: string | null; url?: string | null; @@ -66,17 +64,9 @@ export function formatMediaPlaceholderText(media: readonly MediaPlaceholderTextF : `${tag} (${media.length} ${PLURAL_MEDIA_PLACEHOLDER_LABELS[kind]})`; } -/** - * Environment payload fields consumed by prompt/context builders for inbound media attachments. - */ +/** Legacy environment fields consumed by prompt/context builders. */ export type ChannelInboundMediaPayload = { - MediaPath?: string; - MediaUrl?: string; - MediaType?: string; - MediaPaths?: string[]; - MediaUrls?: string[]; - MediaTypes?: string[]; - MediaTranscribedIndexes?: number[]; + [Key in keyof MediaFactLegacyProjection]: MediaFactLegacyProjection[Key]; }; /** Appends an unavailable-media notice to real caption text, or returns the notice alone. */ @@ -92,26 +82,7 @@ export function formatInboundMediaUnavailableText(params: { return `${body}\n\n${notice}`; } -function alignedStrings(values: Array): string[] | undefined { - if (!values.some(Boolean)) { - return undefined; - } - // Preserve indexes across parallel Media* arrays so transcribed indexes and - // media metadata continue to refer to the same attachment. - return values.map((value) => value ?? ""); -} - -function normalizeKind(value: InboundMediaFacts["kind"] | null | undefined) { - return value ?? undefined; -} - -function mediaType(media: InboundMediaFacts): string | undefined { - return media.contentType ?? media.kind; -} - -/** - * Normalizes plugin-provided attachment facts into the channel turn media shape. - */ +/** Normalizes plugin-provided attachments into ordered runtime facts. */ export function toInboundMediaFacts( media: readonly ChannelInboundMediaInput[] | null | undefined, defaults: { @@ -120,22 +91,10 @@ export function toInboundMediaFacts( transcribed?: (media: ChannelInboundMediaInput, index: number) => boolean; } = {}, ): InboundMediaFacts[] { - if (!Array.isArray(media)) { - return []; - } - return media.map((entry, index) => ({ - path: normalizeString(entry.path), - url: normalizeString(entry.url), - contentType: normalizeString(entry.contentType), - kind: normalizeKind(entry.kind) ?? defaults.kind, - transcribed: entry.transcribed === true || defaults.transcribed?.(entry, index) === true, - messageId: normalizeString(entry.messageId) ?? defaults.messageId, - })); + return normalizeMediaFacts(media, defaults); } -/** - * Projects inbound attachment facts into transcript history without transient turn-only flags. - */ +/** Projects facts into history without transient turn-only fields. */ export function toHistoryMediaEntries( media: readonly ChannelInboundMediaInput[] | null | undefined, defaults: { @@ -152,23 +111,9 @@ export function toHistoryMediaEntries( })); } -/** - * Builds prompt environment media fields while keeping single-item legacy fields populated. - */ +/** Builds the legacy singular/plural environment projection. */ export function buildChannelInboundMediaPayload( media: readonly InboundMediaFacts[] | null | undefined, ): ChannelInboundMediaPayload { - const entries = Array.isArray(media) ? media : []; - const transcribedIndexes = entries - .map((item, index) => (item.transcribed ? index : undefined)) - .filter((index): index is number => index !== undefined); - return { - MediaPath: entries[0]?.path, - MediaUrl: entries[0]?.url ?? entries[0]?.path, - MediaType: entries[0] ? mediaType(entries[0]) : undefined, - MediaPaths: alignedStrings(entries.map((item) => item.path)), - MediaUrls: alignedStrings(entries.map((item) => item.url ?? item.path)), - MediaTypes: alignedStrings(entries.map(mediaType)), - MediaTranscribedIndexes: transcribedIndexes.length > 0 ? transcribedIndexes : undefined, - }; + return projectMediaFacts(media); } diff --git a/src/channels/plugins/media-payload.ts b/src/channels/plugins/media-payload.ts index 9afc4e326164..ed15b5ac6520 100644 --- a/src/channels/plugins/media-payload.ts +++ b/src/channels/plugins/media-payload.ts @@ -1,44 +1,19 @@ -/** - * Input media item used by channel outbound payload builders. - */ -export type MediaPayloadInput = { - path: string; - contentType?: string; -}; +import { + projectMediaFacts, + type MediaFact, + type MediaFactLegacyProjection, +} from "../../media/media-facts.js"; -/** - * Legacy-compatible media payload shape consumed by plugin send helpers. - */ -export type MediaPayload = { - MediaPath?: string; - MediaType?: string; - MediaUrl?: string; - MediaPaths?: string[]; - MediaUrls?: string[]; - MediaTypes?: string[]; -}; +/** Input media item used by channel outbound payload builders. */ +export type MediaPayloadInput = Required> & Pick; -/** - * Builds single-item and list media fields for channel outbound helpers. - */ +/** Legacy-compatible media payload shape consumed by plugin send helpers. */ +export type MediaPayload = Omit; + +/** Builds single-item and list media fields for channel outbound helpers. */ export function buildMediaPayload( mediaList: MediaPayloadInput[], opts?: { preserveMediaTypeCardinality?: boolean }, ): MediaPayload { - const first = mediaList[0]; - const mediaPaths = mediaList.map((media) => media.path); - const rawMediaTypes = mediaList.map((media) => media.contentType ?? ""); - // Some callers need MediaTypes to stay aligned with MediaPaths, including - // blank entries. Others use the compact legacy list of present content types. - const mediaTypes = opts?.preserveMediaTypeCardinality - ? rawMediaTypes - : rawMediaTypes.filter((value): value is string => Boolean(value)); - return { - MediaPath: first?.path, - MediaType: first?.contentType, - MediaUrl: first?.path, - MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined, - MediaUrls: mediaPaths.length > 0 ? mediaPaths : undefined, - MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined, - }; + return projectMediaFacts(mediaList, opts?.preserveMediaTypeCardinality ? "aligned" : "compact"); } diff --git a/src/channels/turn/types.ts b/src/channels/turn/types.ts index 892e91da103e..9d8e236a31b3 100644 --- a/src/channels/turn/types.ts +++ b/src/channels/turn/types.ts @@ -1,4 +1,3 @@ -import type { MediaKind } from "@openclaw/media-core/constants"; import type { CommandTurnKind } from "../../auto-reply/command-turn-context.js"; import type { GetReplyOptions, @@ -25,6 +24,7 @@ import type { DurableFinalDeliveryRequirements, OutboundDeliveryQueuePolicy, } from "../../infra/outbound/deliver.js"; +import type { MediaFact } from "../../media/media-facts.js"; import type { InboundEventKind } from "../inbound-event/kind.js"; import type { CreateChannelReplyPipelineParams } from "../message/reply-pipeline.js"; import type { MessageReceipt } from "../message/types.js"; @@ -135,14 +135,7 @@ export type CommandFacts = { }; /** Inbound media facts supplied to the agent context. */ -export type InboundMediaFacts = { - path?: string; - url?: string; - contentType?: string; - kind?: MediaKind; - transcribed?: boolean; - messageId?: string; -}; +export type InboundMediaFacts = Omit; type MaybePromise = T | Promise; diff --git a/src/media/media-facts.ts b/src/media/media-facts.ts new file mode 100644 index 000000000000..24d2e03f4cd6 --- /dev/null +++ b/src/media/media-facts.ts @@ -0,0 +1,104 @@ +import type { MediaKind } from "@openclaw/media-core/constants"; +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; + +/** One ordered runtime attachment; array position is its alignment identity. */ +export type MediaFact = { + path?: string; + url?: string; + contentType?: string; + kind?: MediaKind; + transcribed?: boolean; + messageId?: string; + workspaceDir?: string; +}; + +export type MediaFactInput = { + [Key in keyof MediaFact]?: MediaFact[Key] | null; +}; + +type MediaFactDefaults = { + kind?: MediaKind; + messageId?: string; + workspaceDir?: string; + transcribed?: (media: TInput, index: number) => boolean; +}; + +export type MediaFactLegacyProjection = { + MediaPath?: string; + MediaUrl?: string; + MediaType?: string; + MediaPaths?: string[]; + MediaUrls?: string[]; + MediaTypes?: string[]; + MediaTranscribedIndexes?: number[]; +}; + +function normalizeMediaFact( + media: TInput, + index: number, + defaults: MediaFactDefaults = {}, +): MediaFact { + const workspaceDir = normalizeOptionalString(media.workspaceDir) ?? defaults.workspaceDir; + return { + path: normalizeOptionalString(media.path), + url: normalizeOptionalString(media.url), + contentType: normalizeOptionalString(media.contentType), + kind: media.kind ?? defaults.kind, + transcribed: media.transcribed === true || defaults.transcribed?.(media, index) === true, + messageId: normalizeOptionalString(media.messageId) ?? defaults.messageId, + ...(workspaceDir ? { workspaceDir } : {}), + }; +} + +export function normalizeMediaFacts( + media: readonly TInput[] | null | undefined, + defaults: MediaFactDefaults = {}, +): MediaFact[] { + return Array.isArray(media) + ? media.map((entry, index) => normalizeMediaFact(entry, index, defaults)) + : []; +} + +function projectStrings( + values: Array, + compact: boolean, + preserveEmptyLists: boolean, +): string[] | undefined { + const projected = compact + ? values.filter((value): value is string => Boolean(value)) + : values.map((value) => value ?? ""); + if (projected.length === 0 || (!preserveEmptyLists && !projected.some(Boolean))) { + return undefined; + } + return projected; +} + +export function projectMediaFacts( + media: readonly MediaFactInput[] | null | undefined, + mode: "channel" | "compact" | "aligned" = "channel", +): MediaFactLegacyProjection { + const entries = Array.isArray(media) ? media : []; + const preserveEmptyLists = mode !== "channel"; + const mediaUrl = (entry: MediaFactInput) => + (mode === "channel" ? (entry.url ?? entry.path) : entry.path) ?? undefined; + const mediaType = (entry: MediaFactInput) => + entry.contentType ?? (mode === "channel" ? entry.kind : undefined) ?? undefined; + const transcribedIndexes = entries.flatMap((entry, index) => (entry.transcribed ? [index] : [])); + return { + MediaPath: entries[0]?.path ?? undefined, + MediaUrl: entries[0] ? mediaUrl(entries[0]) : undefined, + MediaType: entries[0] ? mediaType(entries[0]) : undefined, + MediaPaths: projectStrings( + entries.map((entry) => entry.path), + false, + preserveEmptyLists, + ), + MediaUrls: projectStrings(entries.map(mediaUrl), false, preserveEmptyLists), + MediaTypes: projectStrings(entries.map(mediaType), mode === "compact", preserveEmptyLists), + ...(mode !== "channel" + ? {} + : { + MediaTranscribedIndexes: transcribedIndexes.length > 0 ? transcribedIndexes : undefined, + }), + }; +} diff --git a/src/plugin-sdk/agent-media-payload.ts b/src/plugin-sdk/agent-media-payload.ts index f8e7c2a287a5..9821e785962b 100644 --- a/src/plugin-sdk/agent-media-payload.ts +++ b/src/plugin-sdk/agent-media-payload.ts @@ -1,29 +1,12 @@ -// Agent media payload exports expose media roots and loaders for plugin-facing agent payloads. +import { projectMediaFacts, type MediaFactLegacyProjection } from "../media/media-facts.js"; + export { getAgentScopedMediaLocalRoots } from "../media/local-roots.js"; /** Legacy agent media payload layout consumed by older agent adapters. */ -export type AgentMediaPayload = { - MediaPath?: string; - MediaType?: string; - MediaUrl?: string; - MediaPaths?: string[]; - MediaUrls?: string[]; - MediaTypes?: string[]; -}; +export type AgentMediaPayload = Omit; -/** Convert outbound media descriptors into the legacy agent payload field layout. */ export function buildAgentMediaPayload( mediaList: Array<{ path: string; contentType?: string | null }>, ): AgentMediaPayload { - const first = mediaList[0]; - const mediaPaths = mediaList.map((media) => media.path); - const mediaTypes = mediaList.map((media) => media.contentType).filter(Boolean) as string[]; - return { - MediaPath: first?.path, - MediaType: first?.contentType ?? undefined, - MediaUrl: first?.path, - MediaPaths: mediaPaths.length > 0 ? mediaPaths : undefined, - MediaUrls: mediaPaths.length > 0 ? mediaPaths : undefined, - MediaTypes: mediaTypes.length > 0 ? mediaTypes : undefined, - }; + return projectMediaFacts(mediaList, "compact"); } diff --git a/src/sessions/user-turn-transcript.test.ts b/src/sessions/user-turn-transcript.test.ts index e6b9c4dc9e0d..68a1e16f8627 100644 --- a/src/sessions/user-turn-transcript.test.ts +++ b/src/sessions/user-turn-transcript.test.ts @@ -374,6 +374,22 @@ describe("user turn transcript persistence", () => { }); describe("createUserTurnTranscriptRecorder", () => { + it("accepts and normalizes provider-defined persisted media kinds", () => { + const input: UserTurnInput = { + text: "inspect this attachment", + media: [{ path: " /tmp/provider-media.bin ", kind: " provider/custom-media " }], + }; + const recorder = createUserTurnTranscriptRecorder({ + input, + target: unusedRecorderTarget, + }); + + expect(recorder.message).toMatchObject({ + MediaPath: "/tmp/provider-media.bin", + MediaType: "provider/custom-media", + }); + }); + it("persists fallback user turns only once", async () => { const dir = createTempDir("openclaw-user-turn-recorder-fallback-"); const target = createSqliteTranscriptTarget({ dir }); diff --git a/src/sessions/user-turn-transcript.types.ts b/src/sessions/user-turn-transcript.types.ts index 9380b024278c..1789a68a8e92 100644 --- a/src/sessions/user-turn-transcript.types.ts +++ b/src/sessions/user-turn-transcript.types.ts @@ -4,6 +4,7 @@ import type { SessionTranscriptTurnExpectedState, SessionTranscriptTurnLifecyclePatch, } from "../config/sessions/session-transcript-turn-lifecycle.types.js"; +import type { MediaFactInput } from "../media/media-facts.js"; import type { InputProvenance } from "./input-provenance.js"; type UserTurnSessionEntry = { @@ -13,10 +14,7 @@ type UserTurnSessionEntry = { threadId?: string | number; } & Record; -export type PersistedUserTurnMediaInput = { - path?: string | null; - url?: string | null; - contentType?: string | null; +export type PersistedUserTurnMediaInput = Pick & { kind?: string | null; };