harn-stdlib 0.10.104

Embedded Harn standard library source catalog
Documentation
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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
/**
 * Chat-template tool-call envelopes.
 *
 * Some templates wrap a call in their own envelope — marker pairs, an XML
 * shell, a bare JSON list — instead of the fence the route taught. This module
 * owns recognizing those shells and unwrapping them; what the unwrapped body
 * MEANS is std/llm/tool_parse's business.
 *
 * Split out of std/llm/tool_parse so that module stays a composition layer over
 * the byte-oriented host scanners rather than also carrying every envelope
 * dialect.
 */
import { json_fields } from "std/llm/tool_parse_json_support"
import { empty_parse, protocol_violation, provider_result } from "std/llm/tool_parse_result"

fn __tool_parse_envelope_error(kind: string, detail: string, prose: string) -> dict {
  return {
    matched: true,
    result: empty_parse()
      + {
      tool_parse_errors: [
        "The `<"
          + kind
          + ">` chat-template envelope is malformed: "
          + detail
          + ". Re-emit complete calls as canonical ```tool JSON blocks; "
          + "incomplete entries were not executed.",
      ],
      prose: trim(prose),
    },
  }
}

/**
 * What may stand between a `<tool_call>` opener and its JSON body.
 *
 * Models label the block before the body — the literal word `tool`, the tool's
 * own name, or a template channel token — and frequently omit the closing tag.
 * This is the one owner of that vocabulary: both the envelope guard below and
 * the tagged-grammar body parser ask it what the opener is actually carrying,
 * so the two grammars cannot disagree about which spans are a JSON call.
 *
 * `name_hint` is the label, offered as a fallback name for a body that carries
 * bare arguments and no `name` field.
 *
 * @effects: []
 * @errors: []
 * @api_stability: internal
 */
pub fn tool_call_label_body(body: string) -> dict {
  const trimmed = trim(body)
  if starts_with(trimmed, "{") || starts_with(trimmed, "[") {
    return {ok: true, body: trimmed, name_hint: ""}
  }
  const labeled = regex_captures("^([A-Za-z_][A-Za-z0-9_.\\-]{0,63})[ \\t]*\\r?\\n", trimmed)
  if len(labeled) == 0 {
    return {ok: false}
  }
  const rest = trim(trimmed.slice(labeled[0].end, len(trimmed)))
  if !starts_with(rest, "{") && !starts_with(rest, "[") {
    return {ok: false}
  }
  return {ok: true, body: rest, name_hint: to_string(labeled[0].groups[0])}
}

fn __tool_parse_envelope_call(value, name_hint: string = "") -> dict {
  if type_of(value) != "dict" {
    return {ok: false, error: "each chat-template tool entry must be a JSON object"}
  }
  const fields = json_fields(value)
  const name = if fields.name != "" {
    fields.name
  } else {
    trim(name_hint)
  }
  if name == "" {
    return {ok: false, error: "a JSON tool object was missing a non-empty name"}
  }
  let arguments = fields.arguments
  if type_of(arguments) == "string" {
    arguments = try {
      json_parse(arguments)
    } catch (error) {
      return {ok: false, error: "arguments string did not parse: " + to_string(error)}
    }
  }
  if type_of(arguments) != "dict" {
    return {ok: false, error: "arguments must be a JSON object"}
  }
  return {ok: true, call: {id: "tc_envelope", name: name, arguments: arguments}}
}

fn __tool_parse_envelope_json_list(
  kind: string,
  body: string,
  close: string,
  prose: string,
  name_hint: string = "",
) -> dict {
  const source = trim(body)
  const stream = __host_tool_json_stream(source)
  if len(stream?.values ?? []) == 0 {
    const detail =
      stream?.eof ?? false ? "a JSON tool object ended before its closing `}`" : "expected a JSON tool object, found `"
      + trim(
      body,
    )
      .slice(0, 80)
      + "`"
    return __tool_parse_envelope_error("tool_calls", detail, prose)
  }
  let calls: list<dict> = []
  for row in stream.values {
    const normalized = __tool_parse_envelope_call(row.value, name_hint)
    if !(normalized?.ok ?? false) {
      return __tool_parse_envelope_error("tool_calls", to_string(normalized.error), prose)
    }
    calls = calls.appending(normalized.call)
  }
  let after = trim(source.slice(stream.end, len(source)))
  if starts_with(after, close) {
    after = trim(after.slice(len(close), len(after)))
  }
  // Whatever follows the envelope's objects is NOT this module's to judge. An
  // unclosed envelope runs to end of output, so the tail routinely holds the
  // rest of the turn — including a well-formed call in another grammar. This
  // used to become an "expected a JSON tool object" error that discarded both
  // the tail AND the calls already parsed above, which is how one malformed
  // envelope destroyed every other call in the message. Hand the tail back and
  // let the composition layer parse it; it restores the error if the tail turns
  // out to hold nothing.
  return {matched: true, calls: calls, prose: trim(prose), trailing: after}
}

fn __tool_parse_envelope_markers(body: string, prose: string) -> dict {
  let rest = trim(body)
  let calls: list<dict> = []
  let saw_marker = false
  let marker_active = false
  let marker_completed = false
  while rest != "" {
    if starts_with(rest, "</tool_calls>") {
      rest = trim(rest.slice(len("</tool_calls>"), len(rest)))
      break
    }
    if starts_with(rest, "<tool>") {
      if marker_active {
        return __tool_parse_envelope_error(
          "tool_calls",
          "a `<tool>` marker must be followed by a JSON object before another `<tool>` marker",
          prose,
        )
      }
      saw_marker = true
      marker_active = true
      marker_completed = false
      rest = trim(rest.slice(len("<tool>"), len(rest)))
      continue
    }
    if starts_with(rest, "</tool>") {
      if marker_active || !marker_completed {
        return __tool_parse_envelope_error(
          "tool_calls",
          "found an unmatched `</tool>` close without a preceding `<tool>` marker",
          prose,
        )
      }
      marker_active = false
      marker_completed = false
      rest = trim(rest.slice(len("</tool>"), len(rest)))
      continue
    }
    if !marker_active {
      if marker_completed && starts_with(rest, "{") {
        marker_active = true
      } else {
        const detail =
          saw_marker ? "expected a `<tool>` marker before this JSON object" : "the envelope contained no `<tool>` marker"
        return __tool_parse_envelope_error("tool_calls", detail, prose)
      }
    }
    if !starts_with(rest, "{") {
      return __tool_parse_envelope_error(
        "tool_calls",
        "expected a JSON tool object, found `" + rest.slice(0, 80) + "`",
        prose,
      )
    }
    const object_len = __host_tool_balanced_json_len(rest)
    if object_len == 0 {
      return __tool_parse_envelope_error(
        "tool_calls",
        "a JSON tool object ended before its closing `}`",
        prose,
      )
    }
    const decoded = try {
      json_parse(rest.slice(0, object_len))
    } catch (error) {
      return __tool_parse_envelope_error(
        "tool_calls",
        "a JSON tool object did not parse: " + to_string(error),
        prose,
      )
    }
    const normalized = __tool_parse_envelope_call(decoded)
    if !(normalized?.ok ?? false) {
      return __tool_parse_envelope_error("tool_calls", to_string(normalized.error), prose)
    }
    calls = calls.appending(normalized.call)
    marker_active = false
    marker_completed = true
    rest = trim(rest.slice(object_len, len(rest)))
  }
  if marker_active {
    return __tool_parse_envelope_error(
      "tool_calls",
      "a `<tool>` marker ended without a complete JSON object",
      prose,
    )
  }
  if !saw_marker {
    return __tool_parse_envelope_error(
      "tool_calls",
      "the envelope contained no `<tool>` marker",
      prose,
    )
  }
  return {
    matched: true,
    calls: calls,
    prose: [trim(prose), rest].filter(fn(p) { return p != "" }).join("\n"),
  }
}

fn __tool_parse_envelope_xml(body: string, prose: string) -> dict {
  let rest = trim(body)
  let calls: list<dict> = []
  while rest != "" {
    if starts_with(rest, "</tool_calls>") {
      rest = trim(rest.slice(len("</tool_calls>"), len(rest)))
      break
    }
    const opened = regex_captures("^<([A-Za-z_][A-Za-z0-9_.-]*)>", rest)
    if len(opened) == 0 {
      return __tool_parse_envelope_error(
        "tool_calls",
        "expected a tool-call tag inside `<tool_calls>`, found `" + rest.slice(0, 60) + "`",
        prose,
      )
    }
    const name = to_string(opened[0].groups[0])
    const close = "</" + name + ">"
    const after_open = rest.slice(opened[0].end, len(rest))
    const close_at = after_open.index_of(close)
    const inner = close_at >= 0 ? after_open.slice(0, close_at) : after_open
    let arguments: dict = {}
    let argument_rest = trim(inner)
    while argument_rest != "" {
      const argument_open = regex_captures("^<([A-Za-z_][A-Za-z0-9_.-]*)>", argument_rest)
      if len(argument_open) == 0 {
        return __tool_parse_envelope_error(
          "tool_calls",
          "expected an argument tag inside `<"
            + name
            + ">`, found `"
            + argument_rest.slice(0, 60)
            + "`",
          prose,
        )
      }
      const key = to_string(argument_open[0].groups[0])
      if arguments[key] != nil {
        return __tool_parse_envelope_error(
          "tool_calls",
          "the `<"
            + name
            + ">` call repeated the `<"
            + key
            + ">` argument; a call with an ambiguous duplicate argument is not executed",
          prose,
        )
      }
      const argument_close = "</" + key + ">"
      const value_source = argument_rest.slice(argument_open[0].end, len(argument_rest))
      const argument_close_at = value_source.index_of(argument_close)
      if argument_close_at < 0 {
        return __tool_parse_envelope_error(
          "tool_calls",
          "the `<"
            + key
            + ">` argument tag was not closed with `"
            + argument_close
            + "` before end of output",
          prose,
        )
      }
      arguments[key] = trim(value_source.slice(0, argument_close_at))
      argument_rest = trim(
        value_source.slice(argument_close_at + len(argument_close), len(value_source)),
      )
    }
    if close_at < 0 {
      return __tool_parse_envelope_error(
        "tool_calls",
        "the `<" + name + ">` call tag was not closed with `" + close + "` before end of output",
        prose,
      )
    }
    calls = calls.appending({id: "tc_envelope_xml", name: name, arguments: arguments})
    rest = trim(after_open.slice(close_at + len(close), len(after_open)))
  }
  if len(calls) == 0 {
    return __tool_parse_envelope_error(
      "tool_calls",
      "the `<tool_calls>` envelope contained no tool-call tags",
      prose,
    )
  }
  return {
    matched: true,
    calls: calls,
    prose: [trim(prose), rest].filter(fn(p) { return p != "" }).join("\n"),
  }
}

/**
 * The opener spellings a chat template may wrap a JSON call body in.
 *
 * `<tool>`, `<tool_use>`, and `[[tool]]` are not inventions of this parser's
 * imagination: models reach for them under tool-format pressure and then, when
 * nothing comes back, try another spelling — one mined turn narrates it, saying
 * the call "was malformed" before emitting the same call under a different
 * opener. Recognizing the spelling costs nothing, because the body still has to
 * be a JSON call object to match at all; refusing to recognize it costs the
 * call and teaches the model nothing.
 *
 * Order is not precedence — the EARLIEST opener in the text wins — so
 * `<tool_calls>` still claims a `<tool>` that sits inside it.
 */
const ENVELOPE_OPENERS: list<dict> = [
  {open: "<tool_calls>", close: "</tool_calls>", kind: "tool_calls"},
  {open: "<tool_code>", close: "</tool_code>", kind: "tool_code"},
  {open: "<tool_call>", close: "</tool_call>", kind: "tool_call"},
  {open: "<tool_use>", close: "</tool_use>", kind: "tool_call"},
  {open: "<tool>", close: "</tool>", kind: "tool_call"},
  {open: "[[tool]]", close: "[[/tool]]", kind: "tool_call"},
]

/**
 * An envelope whose body is not JSON at all, read through the caller's body
 * ladder.
 *
 * This module owns where an envelope starts and ends. It does not own what
 * grammar the body is written in, and it used to behave as though it did:
 * requiring JSON meant `[[tool]]look({ ... })[[/tool]]` matched an opener this
 * module knows, failed its JSON check, and was declined all the way back to the
 * stray scan, which said nothing. The ladder arrives as a callback because
 * `tool_parse_body` already imports this module for the label unwrap, so the
 * dependency can only run one way.
 */
fn __tool_parse_envelope_direct_body(
  raw_body: string,
  close: string,
  prose: string,
  parse_body,
) -> dict {
  if parse_body == nil {
    return {matched: false}
  }
  const end = raw_body.index_of(close)
  if end < 0 {
    return {matched: false}
  }
  const parsed = parse_body(trim(raw_body.slice(0, end)))
  if !(parsed?.ok ?? false) || parsed?.call == nil {
    return {matched: false}
  }
  return {
    matched: true,
    calls: [parsed.call],
    prose: trim(prose),
    trailing: trim(raw_body.slice(end + len(close), len(raw_body))),
  }
}

/**
 * Offset of the earliest envelope opener that is not `<tool_call>`, or -1.
 *
 * The tagged lane owns `<tool_call>` and asks this to decide whether an
 * alternate opener gets to the text first. Asking "does the text contain a
 * `<tool_call>` anywhere" instead is what made a mixed message lose a call:
 * one tagged block far down the turn switched envelope reading off for the
 * whole message, including an alternate opener that had come before it.
 *
 * @effects: []
 * @errors: []
 * @api_stability: experimental
 */
pub fn tool_envelope_alternate_opener_at(text: string) -> int {
  let at = -1
  for candidate in ENVELOPE_OPENERS {
    if candidate.open == "<tool_call>" {
      continue
    }
    const found = text.index_of(candidate.open)
    if found >= 0 && (at < 0 || found < at) {
      at = found
    }
  }
  return at
}

/**
 * Read a chat-template tool envelope out of `text`.
 *
 * Finds the earliest known envelope opener, delimits its span, and returns the
 * calls inside it along with the prose before it and whatever trailed after.
 * `{matched: false}` means no opener this module knows, which leaves the text
 * to its caller rather than claiming it.
 *
 * `parse_body` is the caller's body ladder, used for an envelope whose body is
 * not JSON. This module owns where a span starts and ends; it does not own what
 * grammar the body is written in, and the callback is how that stays true
 * without an import cycle.
 *
 * @effects: []
 * @errors: []
 * @api_stability: experimental
 */
pub fn tool_parse_envelope(text: string, parse_body = nil) -> dict {
  let opener = ""
  let kind = ""
  let close_tag = ""
  let at = -1
  for candidate in ENVELOPE_OPENERS {
    const found = text.index_of(candidate.open)
    if found >= 0 && (at < 0 || found < at) {
      at = found
      opener = candidate.open
      kind = candidate.kind
      close_tag = candidate.close
    }
  }
  if at < 0 {
    return {matched: false}
  }
  const prose = text.slice(0, at)
  const raw_body = text.slice(at + len(opener), len(text))
  // A `<tool_call>` opener is only this module's business when it wraps a JSON
  // body. Requiring the body to START with `{` also rejected the common shape
  // where the model labels the block first, so a complete call was discarded
  // with the label as the only discriminator.
  const unwrapped = if kind == "tool_call" {
    tool_call_label_body(raw_body)
  } else {
    {ok: true, body: raw_body, name_hint: ""}
  }
  // A body this module cannot read as JSON is not thereby a non-envelope. The
  // opener already said what the span is; the body's grammar is the caller's
  // ladder to judge.
  // `<tool_call>` is excluded on purpose: the tagged lane owns that spelling and
  // reads its heredoc bodies far better than a single-call ladder can, so
  // declining here is what routes it home. The alternates have no better owner.
  const direct = if (unwrapped?.ok ?? false) || opener == "<tool_call>" {
    {matched: false}
  } else {
    __tool_parse_envelope_direct_body(raw_body, close_tag, prose, parse_body)
  }
  if !(unwrapped?.ok ?? false) && !(direct?.matched ?? false) {
    return {matched: false}
  }
  const body = to_string(unwrapped?.body ?? "")
  const name_hint = to_string(unwrapped?.name_hint ?? "")
  let parsed = if direct?.matched ?? false {
    direct
  } else if kind == "tool_calls" {
    const leading = trim(body)
    if starts_with(leading, "<tool>") || starts_with(leading, "{")
      || starts_with(leading, "</tool>") {
      __tool_parse_envelope_markers(body, prose)
    } else {
      __tool_parse_envelope_xml(body, prose)
    }
  } else {
    __tool_parse_envelope_json_list(kind, body, close_tag, prose, name_hint)
  }
  if parsed?.result != nil {
    return parsed
  }
  const violation = protocol_violation(
    "wrong_tool_format",
    "protocol_violation: a tool call was emitted in a chat-template tool envelope "
      + "while `tool_format` is `json`; accepted this turn, but emit canonical "
      + "```tool JSON blocks next turn.",
  )
  parsed = parsed
    + {result: provider_result(parsed.calls, [], parsed.prose, [violation])}
  return parsed
}