import { jsonBytes } from './json.js'; import { LIMITS } from './types.js'; import { isValidId } from './framing.js'; export function object(x: unknown): x is Record { return !!x && typeof x === 'object' && !Array.isArray(x); } export function keys(x: unknown, required: string[], optional: string[] = []): asserts x is Record { if (!object(x) || required.some(k => !Object.hasOwn(x, k)) || Object.keys(x).some(k => !required.includes(k) && !optional.includes(k))) throw new Error('Invalid fields'); } export function integer(x: unknown, min = 0, max = Number.MAX_SAFE_INTEGER): void { if (!Number.isSafeInteger(x) || (x as number) < min || (x as number) > max) throw new Error('Invalid integer'); } export function text(x: unknown, max: number, nonempty = false): void { if (typeof x !== 'string' || Buffer.byteLength(x) > max || (nonempty && !x.trim())) throw new Error('Invalid text'); } export function warning(x: unknown): void { keys(x, ['code', 'message'], ['tool_call_id', 'name']); text(x.code, 128, true); text(x.message, 1024); if (x.tool_call_id !== undefined && !isValidId(x.tool_call_id)) throw new Error('Invalid warning ID'); if (x.name !== undefined) text(x.name, 1024); } export function envelope(x: any): void { keys(x, ['v', 'type', 'id', 'payload'], ['reply_to']); if (x.v !== 1 || !isValidId(x.id) || !x.id.startsWith('b_')) throw new Error('Invalid envelope'); if (x.type === 'start') { if (Object.hasOwn(x, 'reply_to')) throw new Error('Unexpected correlation'); keys(x.payload, ['prompt', 'system_instruction', 'remaining_ms', 'model']); text(x.payload.prompt, LIMITS.USER_PROMPT_MAX_BYTES, true); text(x.payload.system_instruction, LIMITS.ENVELOPE_ALLOWANCE_BYTES, true); integer(x.payload.remaining_ms, 1, LIMITS.MAX_DEADLINE_MS); const m = x.payload.model; keys(m, ['id', 'context_window_tokens', 'max_output_tokens']); text(m.id, 1024, true); integer(m.context_window_tokens, 1); integer(m.max_output_tokens, 1, m.context_window_tokens); const metadata = { ...x, payload: { ...x.payload, prompt: '' } }; if (jsonBytes(metadata, LIMITS.ENVELOPE_ALLOWANCE_BYTES) > LIMITS.ENVELOPE_ALLOWANCE_BYTES) throw new Error('Start metadata too large'); } else { if (!isValidId(x.reply_to) || !x.reply_to.startsWith('a_')) throw new Error('Invalid correlation'); if (x.type === 'collection_ready') keys(x.payload, []); else if (x.type === 'artifact_ack') { keys(x.payload, ['transfer_id', 'decision', 'warning']); if (!isValidId(x.payload.transfer_id) || !['accept', 'skip', 'stored'].includes(x.payload.decision)) throw new Error('Invalid acknowledgement'); if (x.payload.warning !== null) warning(x.payload.warning); if (x.payload.decision === 'stored' && x.payload.warning !== null) throw new Error('Unexpected stored warning'); } else if (x.type === 'tool_response' || x.type === 'model_response') { keys(x.payload, ['result', 'error']); if (x.payload.error !== null) { keys(x.payload.error, ['code', 'message']); text(x.payload.error.code, 128, true); text(x.payload.error.message, 1024); if (x.payload.result !== null) throw new Error('Invalid error result'); } else { if (!object(x.payload.result)) throw new Error('Invalid success result'); if (x.type === 'model_response') modelResponse(x.payload.result); } if (jsonBytes(x.payload, LIMITS.MODEL_RESULT_PAYLOAD_MAX_BYTES) > LIMITS.MODEL_RESULT_PAYLOAD_MAX_BYTES) throw new Error('Payload too large'); } else throw new Error('Unknown frame'); } } export function modelResponse(x: any): void { keys(x, ['content', 'stop_reason', 'usage'], ['provider_state']); if (!Array.isArray(x.content) || !['stop', 'tool_calls', 'length'].includes(x.stop_reason)) throw new Error('Invalid model result'); const ids = new Set(); for (const c of x.content) { if (c.type === 'text') { keys(c, ['type', 'text']); text(c.text, LIMITS.FINAL_MARKDOWN_MAX_BYTES); } else if (c.type === 'tool_call') { keys(c, ['type', 'id', 'name', 'arguments']); if (!isValidId(c.id) || ids.has(c.id) || !object(c.arguments)) throw new Error('Invalid model call'); ids.add(c.id); text(c.name, 128, true); if (jsonBytes(c.arguments, LIMITS.TOOL_REQUEST_PAYLOAD_MAX_BYTES) > LIMITS.TOOL_REQUEST_PAYLOAD_MAX_BYTES) throw new Error('Tool arguments too large'); } else throw new Error('Invalid content'); } if ((ids.size > 0) !== (x.stop_reason === 'tool_calls') && x.stop_reason !== 'length') throw new Error('Invalid stop reason'); keys(x.usage, ['input_tokens', 'output_tokens']); integer(x.usage.input_tokens); integer(x.usage.output_tokens); if (x.provider_state !== undefined) text(x.provider_state, 64 * 1024); } export function confluenceResult(x: any, tool: string): void { const page = (p: any, extra: string[]) => { keys(p, ['page_id', 'title', 'space', 'url', ...extra]); text(p.page_id, 128, true); if (!/^[0-9]+$/.test(p.page_id)) throw new Error('Invalid page ID'); for (const field of ['title', 'space', 'url']) text(p[field], LIMITS.TOOL_RESULT_PAYLOAD_MAX_BYTES); }; const pagination = (p: any, count: number) => { keys(p, ['offset', 'limit', 'has_more']); integer(p.offset, 0, 10000); integer(p.limit, 1, 50); if (typeof p.has_more !== 'boolean' || count > p.limit || (!count && p.has_more)) throw new Error('Invalid pagination'); }; if (tool === 'confluence_view') { page(x, ['markdown', 'truncated']); text(x.markdown, LIMITS.TOOL_RESULT_PAYLOAD_MAX_BYTES); if (typeof x.truncated !== 'boolean') throw new Error('Invalid truncation'); } else if (tool === 'confluence_search') { keys(x, ['pages', 'pagination']); if (!Array.isArray(x.pages) || x.pages.length > 50) throw new Error('Invalid pages'); for (const p of x.pages) { page(p, ['snippet']); text(p.snippet, LIMITS.TOOL_RESULT_PAYLOAD_MAX_BYTES); } pagination(x.pagination, x.pages.length); } else if (tool === 'confluence_list_spaces') { keys(x, ['spaces', 'pagination']); if (!Array.isArray(x.spaces) || x.spaces.length > 50) throw new Error('Invalid spaces'); for (const s of x.spaces) { keys(s, ['key', 'name']); text(s.key, LIMITS.TOOL_RESULT_PAYLOAD_MAX_BYTES); text(s.name, LIMITS.TOOL_RESULT_PAYLOAD_MAX_BYTES); } pagination(x.pagination, x.spaces.length); } else throw new Error('Invalid Confluence operation'); }