UNPKG

braiin

Version:

Behavioral Reasoning AI for Intelligent Navigation

164 lines (163 loc) 5.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createAnswerStreamFilter = exports.STREAM_END_MARKER = void 0; exports.STREAM_END_MARKER = '[[END]]'; /** * Builds a stateful delta filter that streams the value of the top-level * `"answer"` key of a `finish` action, decoding JSON string escapes on the fly. * * The model keeps emitting strict JSON (`{"action":"finish","answer":"..."}`), * so `enforceJsonOutput` can stay ON — there is no plain-text "tail" format and * no way for protocol JSON or reasoning to leak into the stream: only the * characters inside the `answer` string are forwarded to `onToken`, as soon as * each delta arrives (the only buffered part is the `{"action":..,"answer":"` * envelope prefix). * * Steps without a top-level `answer` key (describe/call/abort) emit nothing, * which makes the filter self-gating across the whole reasoning chain. * * Create a fresh filter per LLM call — state is not meant to be reused. */ const createAnswerStreamFilter = (onToken) => { // Structural scan state (everything outside the answer value). const stack = []; let expectKey = false; // at the current object scope, the next string is a key let inString = false; // scanning a non-answer string token let strEscaped = false; // previous char inside that string was a backslash let curString = ''; // accumulates the current string token (key candidate) let pendingAnswer = false; // root "answer" key seen; its value string is the answer // Answer-streaming state (inside the answer value string). let streaming = false; let ansEscaped = false; let uniRemaining = 0; // hex digits left to read for a \uXXXX escape let uniHex = ''; let done = false; const emitEscaped = (c) => { ansEscaped = false; switch (c) { case 'n': onToken('\n'); break; case 't': onToken('\t'); break; case 'r': onToken('\r'); break; case 'b': onToken('\b'); break; case 'f': onToken('\f'); break; case '"': onToken('"'); break; case '\\': onToken('\\'); break; case '/': onToken('/'); break; case 'u': uniRemaining = 4; uniHex = ''; break; default: onToken(c); // unknown escape: forward literally } }; const feedChar = (c) => { if (done) return; if (streaming) { if (uniRemaining > 0) { uniHex += c; uniRemaining--; if (uniRemaining === 0) { const code = parseInt(uniHex, 16); if (!Number.isNaN(code)) onToken(String.fromCharCode(code)); } return; } if (ansEscaped) { emitEscaped(c); return; } if (c === '\\') { ansEscaped = true; return; } if (c === '"') { streaming = false; done = true; return; } // answer value closed onToken(c); return; } if (inString) { if (strEscaped) { curString += c; strEscaped = false; return; } if (c === '\\') { strEscaped = true; return; } if (c === '"') { inString = false; const atRoot = stack.length === 1 && stack[0] === '{'; if (atRoot && expectKey) { // The string just closed was a key of the root object. pendingAnswer = curString === 'answer'; expectKey = false; } return; } curString += c; return; } switch (c) { case '"': if (pendingAnswer) { streaming = true; pendingAnswer = false; } else { inString = true; curString = ''; } return; case '{': stack.push('{'); expectKey = true; pendingAnswer = false; return; case '[': stack.push('['); pendingAnswer = false; return; case '}': stack.pop(); return; case ']': stack.pop(); return; case ':': return; // keep pendingAnswer as set by the key case ',': expectKey = stack[stack.length - 1] === '{'; pendingAnswer = false; return; default: return; // whitespace and structural noise } }; return (delta) => { if (delta === exports.STREAM_END_MARKER) return; for (const ch of delta) feedChar(ch); }; }; exports.createAnswerStreamFilter = createAnswerStreamFilter;