import type { InbandScanEvent, InbandScanner } from "./types";
const SECTION_OPEN = "";
const SECTION_CLOSE = "";
const TOOL_ELEMENT = /^<([A-Za-z_][\w.-]*)(\s[^<>]*?)?\s*\/>$/s;
const ATTRIBUTE = /([A-Za-z_][\w.-]*)\s*=\s*("([^"]*)"|'([^']*)')/g;
export class QwenXmlInbandScanner implements InbandScanner {
#buffer = "";
#insideSection = false;
feed(chunk: string): InbandScanEvent[] {
this.#buffer += chunk;
return this.#drain(false);
}
flush(): InbandScanEvent[] {
return this.#drain(true);
}
#drain(flush: boolean): InbandScanEvent[] {
const events: InbandScanEvent[] = [];
while (this.#buffer.length > 0) {
if (!this.#insideSection) {
const open = this.#buffer.indexOf(SECTION_OPEN);
if (open >= 0) {
if (open > 0) events.push({ type: "text", text: this.#buffer.slice(0, open) });
this.#buffer = this.#buffer.slice(open + SECTION_OPEN.length);
this.#insideSection = true;
continue;
}
if (flush) {
events.push({ type: "text", text: this.#buffer });
this.#buffer = "";
break;
}
const held = longestSuffixPrefix(this.#buffer, SECTION_OPEN);
const visibleLength = this.#buffer.length - held;
if (visibleLength > 0) {
events.push({ type: "text", text: this.#buffer.slice(0, visibleLength) });
this.#buffer = this.#buffer.slice(visibleLength);
}
break;
}
const close = this.#buffer.indexOf(SECTION_CLOSE);
if (close < 0) {
if (flush) this.#buffer = "";
break;
}
const section = this.#buffer.slice(0, close);
for (const element of section.match(/<[^<>]+\/>/gs) ?? []) {
const call = parseToolElement(element);
if (!call) continue;
const id = `call_${crypto.randomUUID().replace(/-/g, "").slice(0, 24)}`;
events.push({ type: "toolStart", id, name: call.name });
events.push({ type: "toolEnd", id, name: call.name, arguments: call.arguments, rawBlock: element });
}
this.#buffer = this.#buffer.slice(close + SECTION_CLOSE.length);
this.#insideSection = false;
}
return events;
}
}
function parseToolElement(element: string): { name: string; arguments: Record } | undefined {
const match = TOOL_ELEMENT.exec(element);
if (!match) return undefined;
const args: Record = {};
for (const attribute of match[2]?.matchAll(ATTRIBUTE) ?? []) {
args[attribute[1]!] = attribute[3] ?? attribute[4] ?? "";
}
return { name: match[1]!, arguments: args };
}
function longestSuffixPrefix(text: string, target: string): number {
const max = Math.min(text.length, target.length - 1);
for (let length = max; length > 0; length--) {
if (text.endsWith(target.slice(0, length))) return length;
}
return 0;
}