import type { Message, ToolCall } from "../types"; import { buildArgShapes, buildStringArgsResolver, decodeValue, mintToolCallId, partialSuffixOverlap, partialSuffixOverlapAny, type ToolArgShape, } from "./coercion"; import dialectPrompt from "./glm.md" with { type: "text" }; import { assistantTranscriptParts, collectToolResultRun, messageContentText, renderToolResponseResults, stringifyJson, } from "./rendering"; import type { DialectDefinition, DialectRenderOptions, DialectToolResult, InbandScanEvent, InbandScanner, InbandScannerOptions, } from "./types"; const TOOL_OPEN = ""; const TOOL_CLOSE = ""; const ARG_KEY_OPEN = ""; const ARG_KEY_CLOSE = ""; const ARG_VALUE_OPEN = ""; const ARG_VALUE_CLOSE = ""; const RESPONSE_OPEN = ""; const RESPONSE_CLOSE = ""; const THINK_OPEN = ""; const THINK_CLOSE = ""; const OUTSIDE_TAGS = [ TOOL_OPEN, ARG_KEY_OPEN, ARG_KEY_CLOSE, ARG_VALUE_OPEN, ARG_VALUE_CLOSE, RESPONSE_OPEN, RESPONSE_CLOSE, THINK_OPEN, THINK_CLOSE, ] as const; const OUTSIDE_TAGS_NO_THINK = [ TOOL_OPEN, ARG_KEY_OPEN, ARG_KEY_CLOSE, ARG_VALUE_OPEN, ARG_VALUE_CLOSE, RESPONSE_OPEN, RESPONSE_CLOSE, ] as const; const BODY_TAGS = [ARG_KEY_OPEN, TOOL_CLOSE] as const; type State = "outside" | "thinking" | "name" | "body" | "key" | "afterkey" | "value"; interface OpenCall { id: string; name: string; stringArgs: ReadonlySet; arguments: Record; key: string | null; valueRaw: string; rawBlock: string; } interface TagMatch { index: number; tag: string; } export class GLMInbandScanner implements InbandScanner { #buffer = ""; #state: State = "outside"; #call: OpenCall | null = null; #thinking = ""; #parseThinking: boolean; #stringArgs: (toolName: string) => ReadonlySet; constructor(options: InbandScannerOptions = {}) { this.#parseThinking = options.parseThinking !== false; this.#stringArgs = options.stringArgs ?? buildStringArgsResolver(options.tools); } feed(text: string): InbandScanEvent[] { if (text.length === 0) return []; this.#buffer += text; return this.#consume(false); } flush(): InbandScanEvent[] { return this.#consume(true); } #consume(final: boolean): InbandScanEvent[] { const events: InbandScanEvent[] = []; while (this.#buffer.length > 0) { if (this.#state === "outside") { if (!this.#consumeOutside(final, events)) break; continue; } if (this.#state === "thinking") { this.#consumeThinking(final, events); if (this.#state === "thinking") break; continue; } if (this.#state === "name") { if (!this.#consumeName(final, events)) break; continue; } if (this.#state === "body") { if (!this.#consumeBody(final, events)) break; continue; } if (this.#state === "key") { if (!this.#consumeKey(final)) break; continue; } if (this.#state === "afterkey") { if (!this.#consumeAfterKey(final)) break; continue; } if (!this.#consumeValue(final, events)) break; } if (final && this.#state === "thinking") this.#endThinking(events); return events; } #consumeOutside(final: boolean, events: InbandScanEvent[]): boolean { const tags = this.#parseThinking ? OUTSIDE_TAGS : OUTSIDE_TAGS_NO_THINK; const match = findFirstTag(this.#buffer, tags); if (!match) { const hold = final ? 0 : partialSuffixOverlapAny(this.#buffer, tags); const emit = this.#buffer.slice(0, this.#buffer.length - hold); if (emit.length > 0) events.push({ type: "text", text: emit }); this.#buffer = this.#buffer.slice(this.#buffer.length - hold); return false; } if (match.index > 0) events.push({ type: "text", text: this.#buffer.slice(0, match.index) }); this.#buffer = this.#buffer.slice(match.index + match.tag.length); if (match.tag === TOOL_OPEN) { this.#state = "name"; return true; } if (match.tag === THINK_OPEN && this.#parseThinking) { this.#thinking = ""; events.push({ type: "thinkingStart" }); this.#state = "thinking"; return true; } if (match.tag === RESPONSE_OPEN) { this.#buffer = ""; return false; } return true; } #consumeThinking(final: boolean, events: InbandScanEvent[]): void { const close = this.#buffer.indexOf(THINK_CLOSE); if (close === -1) { const hold = final ? 0 : partialSuffixOverlap(this.#buffer, THINK_CLOSE); const emit = this.#buffer.slice(0, this.#buffer.length - hold); this.#emitThinking(emit, events); this.#buffer = this.#buffer.slice(this.#buffer.length - hold); if (final) this.#endThinking(events); return; } this.#emitThinking(this.#buffer.slice(0, close), events); this.#buffer = this.#buffer.slice(close + THINK_CLOSE.length); this.#endThinking(events); this.#state = "outside"; } #consumeName(final: boolean, events: InbandScanEvent[]): boolean { const newline = this.#buffer.indexOf("\n"); const key = this.#buffer.indexOf(ARG_KEY_OPEN); const close = this.#buffer.indexOf(TOOL_CLOSE); const delimiter = minFound(newline, key, close); if (delimiter === -1) { if (!final) return false; this.#beginCall(this.#buffer, events); this.#buffer = ""; this.#endCall(events); return false; } const rawName = this.#buffer.slice(0, delimiter); this.#beginCall(rawName, events); if (delimiter === newline) { this.#appendCallRaw("\n"); this.#buffer = this.#buffer.slice(delimiter + 1); this.#state = "body"; return true; } if (delimiter === key) { this.#appendCallRaw(ARG_KEY_OPEN); this.#buffer = this.#buffer.slice(delimiter + ARG_KEY_OPEN.length); this.#state = "key"; return true; } this.#appendCallRaw(TOOL_CLOSE); this.#buffer = this.#buffer.slice(delimiter + TOOL_CLOSE.length); this.#endCall(events); return true; } #consumeBody(final: boolean, events: InbandScanEvent[]): boolean { this.#appendCallRaw(this.#skipWhitespace()); if (this.#buffer.length === 0) return false; if (this.#buffer.startsWith(ARG_KEY_OPEN)) { this.#appendCallRaw(ARG_KEY_OPEN); this.#buffer = this.#buffer.slice(ARG_KEY_OPEN.length); this.#state = "key"; return true; } if (this.#buffer.startsWith(TOOL_CLOSE)) { this.#appendCallRaw(TOOL_CLOSE); this.#buffer = this.#buffer.slice(TOOL_CLOSE.length); this.#endCall(events); return true; } if (!final && partialSuffixOverlapAny(this.#buffer, BODY_TAGS) === this.#buffer.length) return false; this.#appendCallRaw(this.#buffer[0] ?? ""); this.#buffer = this.#buffer.slice(1); return true; } #consumeKey(final: boolean): boolean { const close = this.#buffer.indexOf(ARG_KEY_CLOSE); if (close === -1) { if (final) this.#dropCall(); return false; } if (this.#call) { this.#call.key = this.#buffer.slice(0, close).trim(); this.#appendCallRaw(this.#buffer.slice(0, close + ARG_KEY_CLOSE.length)); } this.#buffer = this.#buffer.slice(close + ARG_KEY_CLOSE.length); this.#state = "afterkey"; return true; } #consumeAfterKey(final: boolean): boolean { this.#appendCallRaw(this.#skipWhitespace()); if (this.#buffer.length === 0) return false; if (this.#buffer.startsWith(ARG_VALUE_OPEN)) { this.#appendCallRaw(ARG_VALUE_OPEN); this.#buffer = this.#buffer.slice(ARG_VALUE_OPEN.length); if (this.#call) this.#call.valueRaw = ""; this.#state = "value"; return true; } if (!final && ARG_VALUE_OPEN.startsWith(this.#buffer)) return false; this.#appendCallRaw(this.#buffer[0] ?? ""); this.#buffer = this.#buffer.slice(1); return true; } #consumeValue(final: boolean, events: InbandScanEvent[]): boolean { const close = this.#buffer.indexOf(ARG_VALUE_CLOSE); if (close === -1) { const hold = final ? 0 : partialSuffixOverlap(this.#buffer, ARG_VALUE_CLOSE); const emit = this.#buffer.slice(0, this.#buffer.length - hold); this.#streamValue(emit, events); this.#buffer = this.#buffer.slice(this.#buffer.length - hold); if (final) this.#dropCall(); return false; } this.#streamValue(this.#buffer.slice(0, close), events); this.#appendCallRaw(ARG_VALUE_CLOSE); this.#buffer = this.#buffer.slice(close + ARG_VALUE_CLOSE.length); this.#endValue(); this.#state = "body"; return true; } #beginCall(rawName: string, events: InbandScanEvent[]): void { const name = rawName.trim(); if (name.length === 0) { this.#dropCall(); return; } const id = mintToolCallId(); this.#call = { id, name, stringArgs: this.#stringArgs(name), arguments: {}, key: null, valueRaw: "", rawBlock: `${TOOL_OPEN}${rawName}`, }; events.push({ type: "toolStart", id, name }); } #streamValue(chunk: string, events: InbandScanEvent[]): void { const call = this.#call; if (!call || call.key === null || chunk.length === 0) return; call.valueRaw += chunk; call.rawBlock += chunk; events.push({ type: "toolArgDelta", id: call.id, name: call.name, key: call.key, delta: chunk }); } #endValue(): void { const call = this.#call; if (!call || call.key === null) return; call.arguments[call.key] = call.stringArgs.has(call.key) ? call.valueRaw : decodeValue(call.valueRaw); call.key = null; call.valueRaw = ""; } #endCall(events: InbandScanEvent[]): void { const call = this.#call; if (!call) { this.#state = "outside"; return; } events.push({ type: "toolEnd", id: call.id, name: call.name, arguments: call.arguments, rawBlock: call.rawBlock, }); this.#call = null; this.#state = "outside"; } #dropCall(): void { this.#call = null; this.#state = "outside"; } #appendCallRaw(text: string): void { if (this.#call && text.length > 0) this.#call.rawBlock += text; } #emitThinking(delta: string, events: InbandScanEvent[]): void { if (delta.length === 0) return; this.#thinking += delta; events.push({ type: "thinkingDelta", delta }); } #endThinking(events: InbandScanEvent[]): void { events.push({ type: "thinkingEnd", thinking: this.#thinking }); this.#thinking = ""; this.#state = "outside"; } #skipWhitespace(): string { let i = 0; while (i < this.#buffer.length && " \n\t\r".includes(this.#buffer[i]!)) i++; const skipped = this.#buffer.slice(0, i); if (i > 0) this.#buffer = this.#buffer.slice(i); return skipped; } } function findFirstTag(text: string, tags: readonly string[]): TagMatch | null { let best: TagMatch | null = null; for (const tag of tags) { const index = text.indexOf(tag); if (index === -1) continue; if (!best || index < best.index) best = { index, tag }; } return best; } function minFound(...values: readonly number[]): number { let best = -1; for (const value of values) { if (value === -1) continue; if (best === -1 || value < best) best = value; } return best; } function renderToolCall(call: ToolCall, options: DialectRenderOptions = {}): string { return glmInvocation(call, buildArgShapes(options.tools).get(call.name)); } function glmInvocation(call: ToolCall, shape: ToolArgShape | undefined): string { let body = `${TOOL_OPEN}${call.name}`; for (const key in call.arguments) { const value = call.arguments[key]; const rendered = shape?.stringArgs.has(key) && typeof value === "string" ? value : stringifyJson(value); body += `\n${ARG_KEY_OPEN}${key}${ARG_KEY_CLOSE}\n${ARG_VALUE_OPEN}${rendered}${ARG_VALUE_CLOSE}`; } return `${body}\n${TOOL_CLOSE}`; } function renderAssistantToolCalls(calls: readonly ToolCall[], options: DialectRenderOptions = {}): string { const shapes = buildArgShapes(options.tools); return calls.map(call => glmInvocation(call, shapes.get(call.name))).join("\n"); } function renderToolResults(results: readonly DialectToolResult[]): string { return `\n${renderToolResponseResults(results)}\n`; } function renderThinking(text: string): string { if (!text) return ""; return `${THINK_OPEN}\n${text}\n${THINK_CLOSE}`; } function renderTranscript(messages: readonly Message[], options: DialectRenderOptions = {}): string { if (messages.length === 0) return ""; let out = "[gMASK]"; for (let i = 0; i < messages.length; ) { const message = messages[i]!; if (message.role === "assistant") { const parts = assistantTranscriptParts(message); const thinking = parts.thinking ? `\n${renderThinking(parts.thinking)}` : ""; out += `<|assistant|>\n${thinking}${parts.text}${renderAssistantToolCalls(parts.toolCalls, options)}`; i++; continue; } if (message.role === "toolResult") { const run = collectToolResultRun(messages, i); out += `<|observation|>\n${renderToolResponseResults(run.results)}`; i = run.next; continue; } const role = message.role === "developer" ? "system" : message.role; out += `<|${role}|>\n${messageContentText(message.content)}`; i++; } return out; } const definition: DialectDefinition = { dialect: "glm", prompt: dialectPrompt, createScanner: options => new GLMInbandScanner(options), renderToolCall, renderAssistantToolCalls, renderToolResults, renderThinking, renderTranscript, }; export default definition;