/** * Public tool-response contracts shared by the API, SDK, Play bundler, and * runtime. This is a versioned response transformation contract: a change to * the persisted public tool-result shape must introduce a new value here and * have newly built Play artifacts select it. Receipt-cache revisions are * intentionally separate: ordinary response-contract changes do not refresh * durable receipts unless the response transformation changed as well. */ export const V2_TOOL_RESPONSE_CONTRACT = 'v2-tool-response' as const; export const RAW_V2_TOOL_RESPONSE_CONTRACT = 'raw-v2' as const; /** * Deliberate durable-receipt boundary for the raw-v2 response transformation. * Bump only when that transformation changes serialized tool output. Do not * bump for ordinary protocol, authoring, or transport contract changes. */ export const RAW_V2_TOOL_RESPONSE_RECEIPT_REVISION = 'raw-v2-receipt-v1'; const TOOL_RESPONSE_RECEIPT_REVISION_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,63})$/; export type ToolResponseContract = | typeof V2_TOOL_RESPONSE_CONTRACT | typeof RAW_V2_TOOL_RESPONSE_CONTRACT; export type ToolResponseView = 'data' | 'rawV2'; export function isToolResponseContract( value: unknown, ): value is ToolResponseContract { return ( value === V2_TOOL_RESPONSE_CONTRACT || value === RAW_V2_TOOL_RESPONSE_CONTRACT ); } /** * Preserves the distinction between an old artifact with no declared response * contract and an artifact that explicitly selected the legacy V2 contract. * Receipt reuse is controlled separately by `toolResponseReceiptRevision`. */ export function declaredToolResponseContract( value: unknown, ): ToolResponseContract | undefined { if (value == null) return undefined; if (isToolResponseContract(value)) return value; throw new UnsupportedToolResponseContractError(value); } export class UnsupportedToolResponseContractError extends Error { constructor(value: unknown) { super( `Unsupported tool response contract ${String(value)}. Supported contracts: ${V2_TOOL_RESPONSE_CONTRACT}, ${RAW_V2_TOOL_RESPONSE_CONTRACT}.`, ); this.name = 'UnsupportedToolResponseContractError'; } } export class InvalidToolResponseReceiptRevisionError extends Error { constructor(value: unknown) { super( `Tool response receipt revision must be a non-empty static identifier (letters, numbers, '.', '_', or '-'); received ${String(value)}.`, ); this.name = 'InvalidToolResponseReceiptRevisionError'; } } /** Missing means a historical artifact and preserves its original receipt key. */ export function normalizeToolResponseReceiptRevision( value: unknown, ): string | undefined { if (value == null) return undefined; if ( typeof value === 'string' && TOOL_RESPONSE_RECEIPT_REVISION_PATTERN.test(value) ) { return value; } throw new InvalidToolResponseReceiptRevisionError(value); } /** Missing artifact compatibility predates canonical bodies and stays V2. */ export function normalizeToolResponseContract( value: unknown, ): ToolResponseContract { return declaredToolResponseContract(value) ?? V2_TOOL_RESPONSE_CONTRACT; } export function legacyRawFromToolResponseRawV2( rawV2: unknown, view: ToolResponseView, responseMeta?: Record, ): unknown { const legacyRaw = view === 'data' && rawV2 && typeof rawV2 === 'object' && !Array.isArray(rawV2) ? (rawV2 as Record).data : rawV2; // Before raw-v2, an async launch without a data envelope exposed // Deepline's billing summary at `toolResponse.raw.deepline_billing`. // The canonical response keeps it separate from provider data, so reattach // it only to this derived legacy view. const deeplineBilling = responseMeta?.deepline_billing; if ( view === 'rawV2' && deeplineBilling !== undefined && legacyRaw && typeof legacyRaw === 'object' && !Array.isArray(legacyRaw) ) { return { ...(legacyRaw as Record), deepline_billing: deeplineBilling, }; } return legacyRaw; } export function providerMetaFromToolResponseRawV2( rawV2: unknown, view: ToolResponseView, ): Record | undefined { if ( view !== 'data' || !rawV2 || typeof rawV2 !== 'object' || Array.isArray(rawV2) ) { return undefined; } const meta = (rawV2 as Record).meta; return meta && typeof meta === 'object' && !Array.isArray(meta) ? (meta as Record) : undefined; }