blob: afcf3bddea2659d1d24805c8c8bc147d46b2cbe7 [file]
import {
MAX_ATTACHMENT_BYTES,
MAX_READ_IMAGE_BYTES,
READ_IMAGE_TOO_LARGE_MESSAGE,
type AttachmentByteReader,
type StorageRef,
type ToolResultContent,
} from '@maka/core';
import type {
ArtifactAuthorityStore,
ArtifactStore,
DurableArtifactAttachmentReader,
} from './artifact-store.js';
export interface ArtifactAttachmentResourceReader {
readAttachmentResource(
sessionId: string,
artifactId: string,
abortSignal: AbortSignal,
): Promise<ToolResultContent>;
}
/** Read a user-uploaded Artifact without exposing its storage path. */
export function createArtifactAttachmentResourceReader(input: {
artifactStore:
| Pick<ArtifactAuthorityStore, 'getInSession' | 'readTextInSession'>
| Pick<ArtifactStore, 'get' | 'readText'>;
}): ArtifactAttachmentResourceReader {
return Object.freeze({
async readAttachmentResource(
sessionId: string,
artifactId: string,
abortSignal: AbortSignal,
): Promise<ToolResultContent> {
abortSignal.throwIfAborted();
const record =
'getInSession' in input.artifactStore
? (await input.artifactStore.getInSession(sessionId, artifactId)).record
: await input.artifactStore.get(artifactId);
if (!record || record.status !== 'live' || record.source !== 'user_upload') {
throw new Error('Attachment was not found in this Session');
}
if (record.sessionId !== sessionId) {
throw new Error('Attachment was not found in this Session');
}
if (record.kind === 'image') {
if (!record.mimeType) throw new Error('Attachment image has no media type');
return {
kind: 'image',
mimeType: record.mimeType,
ref: { kind: 'session_file', sessionId, relativePath: artifactId },
};
}
if (record.kind === 'pdf') {
throw new Error('PDF attachments cannot be decoded by Read');
}
const read =
'readTextInSession' in input.artifactStore
? await input.artifactStore.readTextInSession(sessionId, artifactId)
: await input.artifactStore.readText(artifactId);
abortSignal.throwIfAborted();
if (!read.ok) throw new Error(`Attachment could not be read: ${read.reason}`);
return { kind: 'text', text: read.text };
},
});
}
export function createAttachmentByteReader(input: {
artifactStore: DurableArtifactAttachmentReader;
sessionId: string;
maxBytes?: number;
}): AttachmentByteReader {
const maxBytes = input.maxBytes ?? MAX_ATTACHMENT_BYTES;
return async (ref) => {
if (ref.kind !== 'session_file') return { ok: false, reason: 'unsupported_ref_kind' };
if (ref.sessionId !== input.sessionId) return { ok: false, reason: 'session_mismatch' };
const result = await input.artifactStore.readDurableAttachmentBinary({
artifactId: ref.relativePath,
sessionId: input.sessionId,
maxBytes,
});
return result.ok
? { ok: true, bytes: Buffer.from(result.base64, 'base64') }
: { ok: false, reason: result.reason };
};
}
export function createReadImageSnapshotter(artifactStore: Pick<ArtifactStore, 'create'>) {
return async (input: {
sessionId: string;
turnId: string;
name: string;
bytes: Uint8Array;
mimeType: string;
}): Promise<Extract<StorageRef, { kind: 'session_file' }>> => {
if (input.bytes.byteLength > MAX_READ_IMAGE_BYTES) {
throw new Error(READ_IMAGE_TOO_LARGE_MESSAGE);
}
const artifact = await artifactStore.create({
sessionId: input.sessionId,
turnId: input.turnId,
name: input.name,
kind: 'image',
content: input.bytes,
mimeType: input.mimeType,
source: 'tool_result',
});
return {
kind: 'session_file',
sessionId: input.sessionId,
relativePath: artifact.id,
};
};
}