File size: 4,570 Bytes
0ed8124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21ad36a
 
0ed8124
21ad36a
0ed8124
 
 
 
 
21ad36a
0ed8124
 
 
 
 
 
 
 
 
 
21ad36a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0ed8124
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
export interface ParsedToolCall {
  name: string;
  arguments: Record<string, unknown>;
  raw: string;
}

export interface ToolCallParseResult {
  calls: ParsedToolCall[];
  content: string;
  incomplete: boolean;
  errors: string[];
}

const OPEN = '<tool_call>';
const CLOSE = '</tool_call>';
const FUNCTION_CLOSE = '</function>';
const PARAMETER_CLOSE = '</parameter>';
const MAX_TOOL_PAYLOAD = 64 * 1024;
const NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_.:-]*$/;

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === 'object' && value !== null && !Array.isArray(value);
}

function parseJsonEnvelope(raw: string): ParsedToolCall {
  const envelope: unknown = JSON.parse(raw);
  if (!isRecord(envelope) || typeof envelope.name !== 'string' || !envelope.name.trim()) {
    throw new Error('tool call must contain a non-empty name');
  }
  let argumentsValue = envelope.arguments;
  if (typeof argumentsValue === 'string') argumentsValue = JSON.parse(argumentsValue);
  if (!isRecord(argumentsValue)) throw new Error('tool call arguments must be an object');
  return { name: envelope.name.trim(), arguments: argumentsValue, raw };
}

function parseFunctionEnvelope(raw: string): ParsedToolCall {
  const functionOpeningEnd = raw.indexOf('>');
  if (!raw.startsWith('<function=') || functionOpeningEnd < 0) {
    throw new Error('tool call must contain a JSON or function envelope');
  }
  const name = raw.slice('<function='.length, functionOpeningEnd).trim();
  if (!NAME_PATTERN.test(name)) throw new Error('tool call must contain a valid function name');
  const functionClosing = raw.lastIndexOf(FUNCTION_CLOSE);
  if (functionClosing < functionOpeningEnd || raw.slice(functionClosing + FUNCTION_CLOSE.length).trim()) {
    throw new Error('function tool call is missing its closing tag');
  }

  const body = raw.slice(functionOpeningEnd + 1, functionClosing);
  const argumentsValue: Record<string, unknown> = {};
  let cursor = 0;
  while (cursor < body.length) {
    while (/\s/u.test(body[cursor] ?? '')) cursor += 1;
    if (cursor >= body.length) break;
    if (!body.startsWith('<parameter=', cursor)) {
      throw new Error('function tool call contains text outside a parameter');
    }
    const parameterOpeningEnd = body.indexOf('>', cursor);
    if (parameterOpeningEnd < 0) throw new Error('function tool call has an incomplete parameter tag');
    const parameterName = body.slice(cursor + '<parameter='.length, parameterOpeningEnd).trim();
    if (!NAME_PATTERN.test(parameterName)) throw new Error('function tool call contains an invalid parameter name');
    if (Object.hasOwn(argumentsValue, parameterName)) throw new Error(`function tool call repeats parameter ${parameterName}`);
    const valueStart = parameterOpeningEnd + 1;
    const valueEnd = body.indexOf(PARAMETER_CLOSE, valueStart);
    if (valueEnd < 0) throw new Error(`function tool call is missing the closing tag for parameter ${parameterName}`);
    argumentsValue[parameterName] = body.slice(valueStart, valueEnd).trim();
    cursor = valueEnd + PARAMETER_CLOSE.length;
  }
  return { name, arguments: argumentsValue, raw };
}

function parseEnvelope(raw: string): ParsedToolCall {
  if (raw.length > MAX_TOOL_PAYLOAD) throw new Error('tool call exceeds 64 KiB');
  return raw.startsWith('<function=') ? parseFunctionEnvelope(raw) : parseJsonEnvelope(raw);
}

export function parseToolCalls(value: string): ToolCallParseResult {
  const calls: ParsedToolCall[] = [];
  const errors: string[] = [];
  const content: string[] = [];
  let cursor = 0;
  let incomplete = false;

  while (cursor < value.length) {
    const opening = value.indexOf(OPEN, cursor);
    if (opening < 0) {
      content.push(value.slice(cursor));
      break;
    }
    content.push(value.slice(cursor, opening));
    const payloadStart = opening + OPEN.length;
    const closing = value.indexOf(CLOSE, payloadStart);
    if (closing < 0) {
      incomplete = true;
      break;
    }
    const raw = value.slice(payloadStart, closing).trim();
    try {
      calls.push(parseEnvelope(raw));
    } catch (error) {
      errors.push(error instanceof Error ? error.message : String(error));
    }
    cursor = closing + CLOSE.length;
  }

  return {
    calls,
    content: content.join('').trim(),
    incomplete,
    errors,
  };
}

export function renderToolResponse(value: unknown): string {
  const serialized = typeof value === 'string' ? value : JSON.stringify(value);
  return `<tool_response>\n${serialized.slice(0, MAX_TOOL_PAYLOAD)}\n</tool_response>`;
}