harn-stdlib 0.10.133

Embedded Harn standard library source catalog
Documentation
import { AgentPresetOptions } from "std/agent/options_types"

/**
 * The option surface, derived from the type that declares it.
 *
 * `agent_loop` options used to be read by lookup alone: each validator
 * reached for the fields it knew about and nothing looked at what else was
 * passed. A misspelled key and a real key at a depth where nothing reads it
 * were both accepted in silence, and the run then reported exactly what a
 * correctly configured run without that feature would report. The two are
 * indistinguishable from the outside, which is how a probe measures the
 * absence of the thing it was written to test.
 *
 * The allowed keys come from `schema_of(AgentPresetOptions)` rather than a list
 * maintained beside the validators. A hand-written list is a second copy of
 * the declaration and drifts from it exactly the way the per-validator field
 * reads already have.
 */
/**
 * Flatten a schema node into the branches that could describe a dict.
 *
 * An option's declared type is often a union (`bool | JudgeConfig`) or an
 * intersection (`AgentSpec` itself). A key is legitimate if any branch
 * declares it, so the branches are unioned rather than intersected: refusing
 * a key some branch accepts would be a false rejection, and this check exists
 * to be trusted.
 *
 * @effects: []
 * @errors: []
 */
fn __option_schema_branches(node: any) -> list<any> {
  if type_of(node) != "dict" {
    return []
  }
  let branches = [node]
  for group in ["all_of", "any_of", "one_of", "union"] {
    const value = node?.[group]
    if type_of(value) == "list" {
      for item in value {
        branches = branches + __option_schema_branches(item)
      }
    } else if type_of(value) == "dict" {
      branches = branches + __option_schema_branches(value)
    }
  }
  return branches
}

/**
 * Every key any branch of this node declares as a property.
 *
 * An empty result means the node declares no properties at all, which is a
 * free-form `dict` option rather than a typed shape. Nothing is known about
 * its keys, so nothing about them can be refused.
 *
 * @effects: []
 * @errors: []
 */
fn __option_declared_keys(node: any) -> list<string> {
  // Concatenate whole key lists rather than appending one name at a time. The
  // element-wise version was quadratic in the number of declared keys and cost
  // 82ms per `agent_loop_options` call, on a path every sub-agent, worker and
  // workflow stage takes.
  let names: list<string> = []
  for branch in __option_schema_branches(node) {
    const properties = branch?.properties
    if type_of(properties) == "dict" {
      names = names + keys(properties)
    }
  }
  if len(names) == 0 {
    return names
  }
  return unique(names)
}

/**
 * The schema for one child key, unioned across the branches that declare it.
 *
 * @effects: []
 * @errors: []
 */
fn __option_child_schema(node: any, key: string) -> any {
  let found = []
  for branch in __option_schema_branches(node) {
    const properties = branch?.properties
    if type_of(properties) == "dict" && contains(properties.keys(), key) {
      found = found + [properties[key]]
    }
  }
  if len(found) == 0 {
    return nil
  }
  if len(found) == 1 {
    return found[0]
  }
  return {union: found}
}

/**
 * Every declared option path, as `[key, dotted path]` pairs.
 *
 * @effects: []
 * @errors: []
 */
fn __option_paths(node: any, path: string) -> list<list<string>> {
  let pairs = []
  for key in __option_declared_keys(node) {
    const child_path = if path == "" {
      key
    } else {
      path + "." + key
    }
    pairs = pairs + [[key, child_path]]
    pairs = pairs + __option_paths(__option_child_schema(node, key), child_path)
  }
  return pairs
}

/**
 * Where else this key is accepted, as a readable clause, or "" if nowhere.
 *
 * @effects: []
 * @errors: []
 */
fn __option_key_accepted_elsewhere(key: string, at_path: string) -> string {
  // Only reached on a rejection, which ends the run, so this walk of the whole
  // declared surface is off the hot path.
  let elsewhere = []
  for pair in __option_paths(schema_of(AgentPresetOptions), "") {
    const name = to_string(pair[0])
    const full = to_string(pair[1])
    if name == key && full != at_path {
      elsewhere = elsewhere + ["`" + full + "`"]
    }
  }
  elsewhere = unique(elsewhere)
  if len(elsewhere) == 0 {
    return ""
  }
  return "; it is read at " + join(elsewhere, " or ")
}

/**
 * Collect every option key nothing on the surface declares.
 *
 * Walks one option dict against its schema node and returns the dotted path of
 * every key no branch declares. A node that declares no properties is a
 * free-form `dict` option: nothing is known about its keys, so the walk stops
 * rather than refusing them.
 *
 * @effects: []
 * @errors: []
 */
fn __collect_unread_keys(value: any, node: any, path: string) -> list<string> {
  if type_of(value) != "dict" {
    return []
  }
  const allowed = __option_declared_keys(node)
  if len(allowed) == 0 {
    return []
  }
  let unread = []
  for key in keys(value) {
    // Runtime-internal plumbing. The stdlib writes these into an option dict
    // itself — `std/agent/lanes` adds `_lane_verdict`, `std/workflow/stage`
    // adds `_nested_kind` — and they arrive here as ordinary caller keys.
    // Declaring them on the public type would advertise private plumbing as
    // API. The cost is that a typo inside the underscore namespace is still
    // accepted, which is a smaller surface than the one this check closes.
    if starts_with(key, "_") {
      continue
    }
    const here = if path == "" {
      key
    } else {
      path + "." + key
    }
    if !contains(allowed, key) {
      unread = unread + [here]
    } else {
      unread = unread + __collect_unread_keys(value[key], __option_child_schema(node, key), here)
    }
  }
  return unread
}

/**
 * Refuse every option key nothing on the surface declares, in one message.
 *
 * A rejection rather than a report, unlike the host event payload census:
 * there the emitters are inside the runtime and a refusal would kill a live
 * path, while a misplaced option is a caller's own configuration and the
 * caller is the one who can fix it. The whole failure here is a signal that
 * was present and read as ordinary, so it has to stop the run.
 *
 * All of them at once, not the first: a caller with three misplaced keys
 * should learn three, and finding them one rebuild at a time is how this
 * check's own bring-up went.
 *
 * @effects: []
 * @errors: [ValueError]
 */
pub fn __validate_agent_loop_option_keys(opts: any) -> nil {
  if type_of(opts) != "dict" {
    return
  }
  const unread = __collect_unread_keys(opts, schema_of(AgentPresetOptions), "")
  if len(unread) == 0 {
    return
  }
  __reject_removed_option_keys(unread)
  let parts = []
  for full in unread {
    const key = __option_path_leaf(to_string(full))
    const at = if to_string(full) == key {
      "the top level"
    } else {
      "`" + __option_path_parent(to_string(full)) + "`"
    }
    parts = parts
      + [
        "`"
          + key
          + "` at "
          + at
          + __option_key_accepted_elsewhere(key, to_string(full)),
      ]
  }
  throw "agent_loop: no option reads "
    + join(parts, ", ")
    + ". A key nothing reads is accepted in silence otherwise, and the run then reports what a run without that option would report."
}

/**
 * The last segment of a dotted option path.
 *
 * @effects: []
 * @errors: []
 */
fn __option_path_leaf(full: string) -> string {
  const parts = split(full, ".")
  return to_string(parts[len(parts) - 1])
}

/**
 * Everything before the last segment of a dotted option path.
 *
 * @effects: []
 * @errors: []
 */
fn __option_path_parent(full: string) -> string {
  const parts = split(full, ".")
  let head = []
  for (index, part) in iter(parts).enumerate() {
    if index < len(parts) - 1 {
      head = head + [to_string(part)]
    }
  }
  return join(head, ".")
}

/**
 * Refuse a key the model-call registry already records as removed, using the
 * registry's own wording.
 *
 * The removed vocabulary has one owner, the registry the dispatch layer reads.
 * That layer only sees a key once a run is under way, which is well past the
 * point a caller can act on it, and the surface check would otherwise reach
 * the key first and report it as unread. A removed key is not unread: it was
 * read until it was taken away, and the registry knows what replaced it.
 *
 * @effects: []
 * @errors: [ValueError]
 */
fn __reject_removed_option_keys(unread: list<string>) {
  const removed = __llm_call_option_registry().removed
  for full in unread {
    const key = __option_path_leaf(to_string(full))
    if removed[key] != nil {
      throw "agent_loop: option `" + key + "` was removed — " + removed[key]
    }
  }
}