/** Small world equivalents of the shared helpers used by messages and lineage. */ import type { ConversationId, EntryRecord, Tx } from '@earendil-works/pi-durable' export { defineEvent } from '@earendil-works/sausage-events' import { defineEvent } from '@earendil-works/sausage-events' export const defineApi = () => (name: string): {name: string; service?: S} => ({name}) export type Device = {device: string; login?: string; address?: string} export const UserInputEvent = defineEvent('user.input') // pi-ai 1.5.0 uses 3.5 characters per estimated text token. export const estimateTextTokens = (text: string) => Math.ceil(text.length / 3.5) export const onCommit = (harness: any, advance: any) => { const observer = harness.observeCommits({advance, end: () => {}}) return () => observer.detach() } export function formatAgo(at: number, now = Date.now(), {ago = false} = {}): string { const minutes = Math.floor(Math.max(0, now - at) / 60000) const unit = (value: number, suffix: string) => `${value}${suffix}${ago ? ' ago' : ''}` if (minutes < 1) return ago ? 'just now' : 'now' if (minutes < 60) return unit(minutes, 'm') if (minutes < 60 * 24) return unit(Math.floor(minutes / 60), 'h') if (minutes < 60 * 24 * 7) return unit(Math.floor(minutes / 60 / 24), 'd') const date = new Date(at) return date.toLocaleDateString(undefined, date.getFullYear() === new Date(now).getFullYear() ? {month: 'short', day: 'numeric'} : {month: 'short', year: '2-digit'}) } export const imageMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'] export const maxImageBytes = 2 * 1024 * 1024, maxImages = 4 export function imagesOf(entry: EntryRecord | undefined) { return (entry?.model ?? []).flatMap(message => (message.role === 'user' || message.role === 'toolResult') && typeof message.content !== 'string' ? message.content.flatMap(block => block.type === 'image' ? [{type: 'image' as const, data: block.data, mimeType: block.mimeType}] : []) : []) } export async function latestImagesEntry(tx: Tx, conversationId: ConversationId) { let cursor do { const page = await tx.scanEntries({conversationId}, 200, cursor) for (const entry of page.items) { if (imagesOf(entry).length) return entry if (entry.input || entry.kind === 'pi.user') throw new Error('images: no images in this turn') } cursor = page.next } while (cursor) throw new Error('images: no images in this turn') }