harn-stdlib 0.10.169

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
/**
 * std/llm/tool_shape — did the parser drop something the model MEANT as a tool call?
 *
 * Import with: import { tool_shape_findings, tool_shape_feedback, ToolShapeFinding }
 *   from "std/llm/tool_shape"
 *
 * WHY THIS MODULE EXISTS. The text parser recognizes tool syntax for a fixed set
 * of wrapper tags. Every other dialect a model emits — and models invent them
 * constantly, because their chat templates differ — falls through to prose: zero
 * calls, zero violations, zero events. The turn dies looking like the model
 * simply chose not to act, and stall detection and the governor then blame the
 * model for a parse boundary that ate its work.
 *
 * The obvious repair is to add each newly-observed dialect to the parser. That
 * is a treadmill: it fixes the dialect in front of you and leaves the next one
 * just as silent. So this module inverts the question. The parser reports what
 * it consumed without producing a call (`dropped`); this module asks whether any
 * of it has the SHAPE of a call.
 *
 * SHAPE, NOT VOCABULARY. Nothing here matches a list of known tag names. A
 * fragment is suspicious because it is *structured like an invocation* — a tag
 * wrapping a call expression, an object naming a tool and its arguments, markup
 * carrying a callee. A dialect nobody has seen yet trips the same checks with no
 * change to this file, which is the property that kills the class rather than
 * the instance.
 *
 * FALSE POSITIVES ARE THE REAL COST. A finding becomes corrective feedback on a
 * paid turn, so merely *mentioning* tool syntax must not trip it: prose that
 * says "wrap calls in <tool_call>" carries no body and matches nothing here.
 * Every check below requires an argument-bearing body, not a name.
 */
import { tool_call_shape } from "std/llm/call_shape"
import { tool_fence_info_opens_call } from "std/llm/dialects"
import { ProtocolViolation, protocol_violation } from "std/llm/tool_parse_result"

/** One dropped fragment as reported by the parser. */
pub type DroppedFragment = {
  /** The text the parser consumed without producing a call. */
  text: string,
  /** Why it was consumed: `salvaged_as_prose` or `fenced_narration`. */
  reason: string,
}

/** A dropped fragment that is shaped like a tool call. */
pub type ToolShapeFinding = {
  /**
   * Which shape matched: `tagged_call_body`, `json_name_arguments`, or
   * `call_markup`. Stable identifiers, safe to branch on.
   */
  shape: string,
  /** The parser's reason for dropping the fragment. */
  dropped_reason: string,
  /** A bounded excerpt of the offending text, for events and feedback. */
  excerpt: string,
}

/**
 * Response tags that are part of the protocol. A fragment built only from
 * these is the model using the contract, not an unrecognized dialect.
 */
const __TOOL_SHAPE_PROTOCOL_TAGS = [
  "tool_call",
  "toolcall",
  "assistant_prose",
  "assistantprose",
  "user_response",
  "userresponse",
  "done",
  // The purpose-label declaration (`std/agent/purpose_labels`,
  // PURPOSE_LABEL_PATTERN). Its body is prose that often names functions, so
  // without this a label next to a valid call read as a broken tool call.
  "purpose",
]

/**
 * Longest excerpt carried into a violation. Long enough to identify the
 * dialect, short enough not to replay a whole response back at the model.
 */
const __TOOL_SHAPE_EXCERPT_LIMIT = 240

/**
 * Excerpt starting at the structural trigger, so the feedback shows the model
 * its unrecognized block rather than whatever benign fragment the parser
 * happened to drop first in the same response.
 */
fn __tool_shape_excerpt(text: string, at: int) -> string {
  const prefix = at > 0 ? "…" : ""
  const collapsed = trim(text.slice(at, len(text)))
  if len(collapsed) <= __TOOL_SHAPE_EXCERPT_LIMIT {
    return prefix + collapsed
  }
  return prefix + collapsed.slice(0, __TOOL_SHAPE_EXCERPT_LIMIT) + "…"
}

/**
 * Does `body` carry arguments, rather than merely naming something?
 *
 * This is the guard that keeps prose *about* tool syntax from being reported
 * as tool syntax. A call expression (`name(`) or an object literal with at
 * least one `key:`/`"key":` pair counts; a bare word does not.
 */
fn __tool_shape_body_has_arguments(body: string) -> bool {
  const trimmed = trim(body)
  if trimmed == "" {
    return false
  }
  if regex_match("[A-Za-z_][A-Za-z0-9_]*\\s*\\(", trimmed) != nil {
    return true
  }
  return regex_match("\\{[^{}]*\"?[A-Za-z_][A-Za-z0-9_]*\"?\\s*:", trimmed) != nil
}

/** Half-open character offsets including the opening and closing fence markers. */
pub type SourceFenceSpan = {start: int, end: int}

/**
 * Source-language fence spans owned by the same classification as findings.
 *
 * An unlabeled fence is commonly a provider wrapper around an attempted call,
 * so it remains eligible. A labeled fence is an authored example/source block:
 * treating its contents as executable intent would turn documentation into a
 * corrective model turn.
 *
 * The one exception is a label that opens a tool block. `tool` is a well-formed
 * language-shaped label, so the pattern below reads Harn's OWN call fence as
 * documentation and the near-miss detector goes silent on exactly the calls a
 * model was taught to write. Ask the dialect vocabulary instead of guessing
 * from the label's shape.
 * @effects: []
 * @errors: []
 * @api_stability: internal
 */
pub fn __tool_shape_source_fences(text: string) -> list<SourceFenceSpan> {
  const markers = regex_captures("(?m)^```", text)
  let spans: list<SourceFenceSpan> = []
  for index in range(0, len(markers)) {
    const marker = markers[index]
    const start = marker.start
    const next = markers[index + 1]
    const after_marker = text.slice(marker.end, next?.start ?? len(text))
    const newline_at = after_marker.index_of("\n")
    const label = trim(newline_at >= 0 ? after_marker.slice(0, newline_at) : after_marker)
    if tool_fence_info_opens_call(label)
      || regex_match("^[A-Za-z][A-Za-z0-9_+.-]*$", label) == nil {
      continue
    }
    const end = next?.end ?? len(text)
    const previous = spans[len(spans) - 1]
    if previous != nil && previous.end > start {
      spans[len(spans) - 1] = {start: previous.start, end: end}
    } else {
      spans.push({start: start, end: end})
    }
  }
  return spans
}

fn __tool_shape_in_labeled_fence(text: string, at: int) -> bool {
  return __tool_shape_source_fences(text).any(fn(span) { return at > span.start && at < span.end })
}

/**
 * An angle tag wrapping a body that carries arguments.
 *
 * Catches the whole family of single-tag dialects (`<tool>`, `<call>`,
 * `<action>`, and whatever comes next) without naming any of them: the tag name
 * is captured, not compared against a list, and only the protocol's own tags
 * are excluded.
 * Returns the char position of the matching open tag, or -1.
 */
fn __tool_shape_tagged_call(text: string) -> int {
  // Open and close tags are matched separately and paired here, because the
  // regex engine has no backreferences — `</\1>` is not expressible. Pairing in
  // Harn also keeps the tag name available for the protocol-tag exclusion.
  for opened in regex_captures("(?is)<([A-Za-z_][A-Za-z0-9_.:-]*)\\b[^>]*>", text) {
    const at = opened?.start ?? 0
    if __tool_shape_in_labeled_fence(text, at) {
      continue
    }
    const tag = lowercase(trim((opened?.groups ?? [])[0] ?? ""))
    if tag == "" || __TOOL_SHAPE_PROTOCOL_TAGS.contains(tag) {
      continue
    }
    // `start`/`end` are character offsets, matching `slice`/`index_of`/`len`.
    const rest = text.slice(opened?.end ?? 0, len(text))
    const close_at = rest.index_of("</" + tag + ">")
    if close_at < 0 {
      continue
    }
    if __tool_shape_body_has_arguments(rest.slice(0, close_at)) {
      return at
    }
  }
  return -1
}

/**
 * An object that names a tool and supplies its arguments.
 *
 * This is the native/bare-JSON dialect family: `{"name": ..., "arguments": ...}`
 * and its spellings. Both halves are required — a `"name"` key alone appears in
 * plenty of ordinary JSON a model might legitimately be discussing.
 * Returns the char position of the tool-naming key, or -1.
 */
fn __tool_shape_json_call(text: string) -> int {
  if !text.contains("{") {
    return -1
  }
  if regex_match("(?i)\"(?:arguments|args|parameters|input|params)\"\\s*:", text) == nil {
    return -1
  }
  for named in regex_captures("(?i)\"(?:name|tool|tool_name|recipient)\"\\s*:", text) {
    const at = named?.start ?? 0
    if !__tool_shape_in_labeled_fence(text, at) {
      return at
    }
  }
  return -1
}

/**
 * Chat-template markup that carries a callee.
 *
 * `<function=NAME>`, `<invoke name="NAME">`, and the `[TOOL_CALLS]` /
 * `<|recipient|>` frame markers count only when their body also carries
 * arguments. The opener alone is common in prose discussing the protocol.
 * Returns the char position of the markup, or -1.
 */
fn __tool_shape_call_markup(text: string) -> int {
  // The attribute part is optional: a chat-template envelope names the callee
  // in the JSON body and marks each entry with a bare `<tool>`. Requiring `=`
  // or whitespace after the tag name left that opener invisible here, so a
  // truncated envelope — whose body loses the `name` key to the truncation and
  // so escapes the JSON check below as well — was dropped with no finding at
  // all. The argument-bearing body test still gates every match.
  for named in regex_captures("(?i)<(function|invoke|tool)\\s*([=\\s][^>]*)?>", text) {
    const at = named?.start ?? 0
    if __tool_shape_in_labeled_fence(text, at) {
      continue
    }
    const body = text.slice(named?.end ?? len(text), len(text))
    const groups = named?.groups ?? []
    // A bare, attribute-less opener that IS closed is an ordinary single-tag
    // call block, and `__tool_shape_tagged_call` already names that shape.
    // Claiming it here would silently move a stable shape identifier, so only
    // the unclosed form — the one nothing else can see — is taken.
    if to_string(groups[1] ?? "") == ""
      && body.index_of("</" + lowercase(to_string(groups[0] ?? "")) + ">")
        >= 0 {
      continue
    }
    if __tool_shape_body_has_arguments(body)
      || regex_match("(?i)<parameter(?:\\s|=|>)", body) != nil {
      return at
    }
  }
  for framed in regex_captures("(?i)\\[TOOL_CALLS\\]|<\\|recipient\\|>|<\\|channel\\|>", text) {
    const at = framed?.start ?? 0
    if __tool_shape_in_labeled_fence(text, at) {
      continue
    }
    const body = text.slice(framed?.end ?? len(text), len(text))
    if __tool_shape_body_has_arguments(body)
      || regex_match("(?i)\"(?:arguments|args|parameters|input|params)\"\\s*:", body)
        != nil {
      return at
    }
  }
  return -1
}

/** Which shape matched, and where in `text` its structural trigger sits. */
type ToolShapeMatch = {shape: string, at: int}

fn __tool_shape_of(text: string) -> ToolShapeMatch {
  const markup_at = __tool_shape_call_markup(text)
  if markup_at >= 0 {
    return {shape: "call_markup", at: markup_at}
  }
  const tagged_at = __tool_shape_tagged_call(text)
  if tagged_at >= 0 {
    return {shape: "tagged_call_body", at: tagged_at}
  }
  const json_at = __tool_shape_json_call(text)
  if json_at >= 0 {
    return {shape: "json_name_arguments", at: json_at}
  }
  return {shape: "", at: -1}
}

/** All dropped fragments of a response, rejoined; reason of the first kept. */
type ToolShapeRun = {text: string, reason: string}

/**
 * Rejoin every dropped fragment of the response, in document order.
 *
 * The parser drops at the granularity it scans — inside a fenced block that is
 * one line at a time, and the lines of one block can carry *different* drop
 * reasons (the tag lines fall out of the tag scanner, the argument line out of
 * the bare-call sniff). A fenced dialect therefore arrives as `<tool>`,
 * `look({ ... })`, `</tool>` — fragments none of which is an invocation on its
 * own, though together they plainly are. Reasons are metadata about *why* each
 * slice was dropped, not boundaries in what the model wrote, so rejoining
 * ignores them; splitting on them is exactly what re-shatters the block.
 *
 * Fragments arrive in document order. Distant fragments of one response do get
 * joined into a single run, which in principle could assemble a call shape out
 * of unrelated pieces — but every shape check still requires an
 * argument-bearing body inside matched structure, and the cost of the rare
 * contrived match is one bounded feedback line, not a lost call.
 */
fn __tool_shape_runs(dropped: list<DroppedFragment>) -> list<ToolShapeRun> {
  let text = ""
  let reason = ""
  for fragment in dropped {
    if trim(fragment.text) == "" {
      continue
    }
    if text == "" {
      reason = fragment.reason
    } else {
      text = text + "\n"
    }
    text = text + fragment.text
  }
  if text == "" {
    return []
  }
  return [{text: text, reason: reason}]
}

/**
 * Findings for every run of dropped text that is shaped like a tool call.
 *
 * Empty when the parser dropped nothing suspicious, which is the common case:
 * most dropped text is genuine narration.
 *
 * @effects: []
 * @errors: []
 * @api_stability: experimental
 */
pub fn tool_shape_findings(dropped: list<DroppedFragment>) -> list<ToolShapeFinding> {
  let findings = []
  for run in __tool_shape_runs(dropped) {
    const matched = __tool_shape_of(run.text)
    if matched.shape == "" {
      continue
    }
    findings.push(
      {
        shape: matched.shape,
        dropped_reason: run.reason,
        excerpt: __tool_shape_excerpt(run.text, matched.at),
      },
    )
  }
  return findings
}

/**
 * Project structural findings into the shared typed violation contract.
 *
 * `tool_format` is the turn's pin, and the syntax this teaches comes from
 * `std/llm/call_shape` rather than from a delimiter written out here. This
 * corrective fires on every pin — the dropped span is by definition not in the
 * grammar the turn was parsed with — so a hard-coded form is wrong for every
 * session on another format. Observed live: this message told a model to emit
 * `<tool_call>name({ ... })</tool_call>` while every other corrective in the
 * same session taught a ```tool fenced JSON object.
 *
 * @effects: []
 * @errors: []
 * @api_stability: experimental
 */
pub fn tool_shape_violations(
  findings: list<ToolShapeFinding>,
  tool_format: string? = "",
) -> list<ProtocolViolation> {
  const call_syntax = tool_call_shape(tool_format).call_syntax
  return findings.map(
    fn(finding) { return protocol_violation(
      "unparsed_tool_syntax",
      "unparsed_tool_syntax: text shaped like a tool call was not "
        + "recognized as one and did not run. Emit every call as "
        + call_syntax
        + ". Unrecognized fragment: "
        + finding.excerpt,
      finding.excerpt,
      finding.dropped_reason,
    ) },
  )
}

/**
 * Corrective feedback for the model, one line per finding.
 *
 * Phrased as the contract it should have used, because the model can act on
 * that; the shape identifier is for events, not for the model.
 *
 * @effects: []
 * @errors: []
 * @api_stability: experimental
 */
pub fn tool_shape_feedback(
  findings: list<ToolShapeFinding>,
  tool_format: string? = "",
) -> list<string> {
  return tool_shape_violations(findings, tool_format).map(
    fn(violation) { return violation.message },
  )
}