/** Resolve immutable image references within the sender's visible history. */ import { int, lit, union } from '@earendil-works/pi-schema' import type { ConversationId, EntryId, EntryRecord, Tx } from '@earendil-works/pi-durable' import { imagesOf, latestImagesEntry } from './compat.ts' import { imageMimeTypes, maxImageBytes, maxImages } from './compat.ts' /** * Which images to forward: `latest`, the newest entry with images in the sender's current turn (its input or a tool * result such as a screenshot since), or an entry's id. */ export type ImagesRef = 'latest' | number export const SCAN_PAGE = 200 /** Resolve `ref` against conversation `from`'s visible history to the entry holding the images, checking the limits. */ export async function resolveImages(tx: Tx, from: ConversationId, ref: ImagesRef) { let entry: EntryRecord | undefined if (ref === 'latest') entry = await latestImagesEntry(tx, from) else { if (!Number.isInteger(ref) || ref < 1) throw new Error('images must be "latest" or the id of an entry with images') entry = ( await tx.scanEntries( { conversationId: from, minEntryId: ref as EntryId, maxEntryId: ref as EntryId, }, 1, ) ).items[0] if (!entry) throw new Error(`images: entry ${ref} is not in this conversation`) } const images = imagesOf(entry) if (!images.length) throw new Error(`images: entry ${entry.id} has no images`) if (images.length > maxImages) throw new Error( `images: entry ${entry.id} has ${images.length} images; at most ${maxImages} go along`, ) for (const image of images) if ( !imageMimeTypes.includes(image.mimeType) || (image.data.length * 3 / 4 - (image.data.endsWith('==') ? 2 : image.data.endsWith('=') ? 1 : 0)) > maxImageBytes ) throw new Error(`images: entry ${entry.id} has an unsupported image or one over 2 MiB`) return { entry: entry.id, count: images.length } } export const imagesParameter = union([lit('latest'), int({ minimum: 1 })], { description: 'Forward images from your conversation: "latest" for those of this turn (your current input, or a tool result such as a screenshot since; it fails when there are none), or an entry id for older ones.', })