harn-stdlib 0.8.163

Embedded Harn standard library source catalog
Documentation
import { agent_typed_output_checkpoint } from "std/agent/primitives"

type MissingToolCallAction = "tool_call_intended" | "no_tool_call_intended" | "ambiguous"

type MissingToolCallTool = {name: string, description?: string}

type MissingToolCallOptions = {
  enabled?: bool,
  provider?: string,
  model?: string,
  confidence_threshold?: float,
  max_tokens?: int,
  temperature?: float,
  classifier?: any,
  top_p?: float,
  seed?: int,
  reasoning_effort?: string,
  timeout?: any,
}

type MissingToolCallVerdict = {
  action: MissingToolCallAction,
  original_action?: string,
  tool_name: string,
  confidence: float,
  confidence_threshold?: float,
  evidence: string,
  language?: string,
  classifier_kind?: string,
  model?: string,
  typed_checkpoint?: any,
  error?: string,
}

fn __missing_tool_call_string(value) -> string {
  if value == nil {
    return ""
  }
  return trim(to_string(value))
}

fn __missing_tool_call_float(value, fallback) {
  let parsed = to_float(value ?? fallback)
  if parsed == nil {
    return fallback
  }
  if parsed < 0.0 {
    return 0.0
  }
  if parsed > 1.0 {
    return 1.0
  }
  return parsed
}

fn __missing_tool_call_threshold(value) -> float {
  let threshold = to_float(value ?? 0.65)
  if threshold == nil || threshold < 0.0 || threshold > 1.0 {
    throw "missing_tool_call_classifier: confidence_threshold must be between 0.0 and 1.0"
  }
  return threshold
}

fn __missing_tool_call_candidate_tool_entries(payload) {
  let registry = payload?.tools
  if type_of(registry) == "dict" {
    return registry?.tools ?? []
  }
  if type_of(registry) == "list" {
    return registry
  }
  return payload?.candidate_tools ?? []
}

fn __missing_tool_call_candidate_tools(payload) -> list<MissingToolCallTool> {
  var out = []
  var names = []
  for entry in __missing_tool_call_candidate_tool_entries(payload) {
    let name = __missing_tool_call_string(entry?.name ?? entry?.function?.name)
    if name == "" || contains(names, name) {
      continue
    }
    names = names.push(name)
    out = out
      .push(
      {
        name: name,
        description: __missing_tool_call_string(entry?.description ?? entry?.function?.description),
      },
    )
  }
  for name in payload?.tool_names ?? [] {
    let text = __missing_tool_call_string(name)
    if text != "" && !contains(names, text) {
      names = names.push(text)
      out = out.push({name: text, description: ""})
    }
  }
  return out
}

fn __missing_tool_call_tool_names(candidate_tools: list<MissingToolCallTool>) -> list<string> {
  var names = []
  for entry in candidate_tools {
    let name = __missing_tool_call_string(entry.name)
    if name != "" && !contains(names, name) {
      names = names.push(name)
    }
  }
  return names
}

fn __missing_tool_call_tool_name(value, candidate_tools: list<MissingToolCallTool>) -> string {
  let requested = __missing_tool_call_string(value)
  if requested == "" {
    return ""
  }
  for name in __missing_tool_call_tool_names(candidate_tools) {
    if requested == name || lowercase(requested) == lowercase(name) {
      return name
    }
  }
  return ""
}

fn __missing_tool_call_schema() {
  return {
    type: "object",
    properties: {
      action: {type: "string", description: "One of: tool_call_intended, no_tool_call_intended, ambiguous."},
      tool_name: {
        type: "string",
        description: "Exact candidate tool name when action is tool_call_intended; otherwise empty.",
      },
      confidence: {type: "number", description: "Confidence from 0.0 to 1.0."},
      evidence: {type: "string", description: "Brief reason grounded in the assistant text."},
      language: {type: "string", description: "Detected language or mixed/unknown."},
    },
    required: ["action", "tool_name", "confidence", "evidence"],
  }
}

fn __missing_tool_call_prompt(payload, candidate_tools) {
  return "You classify assistant text before a tool-dispatch step.\n\n"
    + "The parser already found ZERO valid tool calls in this turn. Decide whether the assistant text "
    + "is nevertheless expressing an intent to invoke one of the candidate tools.\n\n"
    + "Assistant text:\n"
    + __missing_tool_call_string(payload?.text ?? payload?.visible_text)
    + "\n\nCandidate tools JSON:\n"
    + json_stringify(candidate_tools)
    + "\n\nReturn JSON exactly matching this shape: "
    + "{\"action\":\"tool_call_intended|no_tool_call_intended|ambiguous\",\"tool_name\":\"\",\"confidence\":0.0,\"evidence\":\"...\",\"language\":\"...\"}\n\n"
    + "Rules:\n"
    + "- Use tool_call_intended only when the assistant text says it is about to, trying to, or intending to use a candidate tool, even if the text has typos or is not in English.\n"
    + "- Choose tool_name from Candidate tools exactly; leave it empty if no single candidate is clear.\n"
    + "- Use no_tool_call_intended for final answers, status summaries, questions, or ordinary explanation.\n"
    + "- Use ambiguous when the intent or target tool is unclear. Do not infer intent from the original user task alone.\n"
}

fn __missing_tool_call_action(value) -> MissingToolCallAction {
  let action = __missing_tool_call_string(value)
  if action == "tool_call_intended" || action == "no_tool_call_intended" || action == "ambiguous" {
    return action
  }
  return "ambiguous"
}

fn __missing_tool_call_normalize(raw, candidate_tools: list<MissingToolCallTool>, threshold: float) -> MissingToolCallVerdict {
  if type_of(raw) != "dict" {
    return {
      action: "ambiguous",
      original_action: "invalid",
      tool_name: "",
      confidence: 0.0,
      confidence_threshold: threshold,
      evidence: "classifier returned " + type_of(raw) + ", not a dict",
    }
  }
  let original_action = __missing_tool_call_action(raw?.action)
  let confidence = __missing_tool_call_float(raw?.confidence, 0.0)
  let tool_name = __missing_tool_call_tool_name(raw?.tool_name ?? raw?.tool, candidate_tools)
  let action = if original_action == "tool_call_intended" && confidence >= threshold && tool_name != "" {
    "tool_call_intended"
  } else if original_action == "no_tool_call_intended" && confidence >= threshold {
    "no_tool_call_intended"
  } else {
    "ambiguous"
  }
  let evidence = __missing_tool_call_string(raw?.evidence ?? raw?.reason ?? raw?.reasoning)
  return raw
    + {
    action: action,
    original_action: original_action,
    tool_name: tool_name,
    confidence: confidence,
    confidence_threshold: threshold,
    evidence: if evidence == "" {
      "no evidence provided"
    } else {
      evidence
    },
  }
}

fn __missing_tool_call_fail_open(error, threshold: float) -> MissingToolCallVerdict {
  return {
    action: "ambiguous",
    original_action: "error",
    tool_name: "",
    confidence: 0.0,
    confidence_threshold: threshold,
    evidence: "missing tool call classifier failed: " + to_string(error),
    error: to_string(error),
  }
}

/**
 * missing_tool_call_classifier returns the post-turn classifier used by
 * agent_loop text-mode recovery. It decides whether a turn with no parsed tool
 * calls was probably meant to call one, without language-specific substring
 * lists.
 *
 * The agent loop constructs this classifier by default for real provider-backed
 * text-mode loops and passes through the active loop provider/model unless this
 * config overrides them. Pass `missing_tool_call_recovery: false` to disable it
 * or `missing_tool_call_recovery: { ... }` to configure classifier-specific
 * settings. Mock/custom-caller tests can pass `classifier` inside that config
 * for deterministic behavior; the live path uses a schema-bound typed-output
 * checkpoint and fails open to `ambiguous`.
 *
 * @effects: [llm.call]
 * @errors: []
 * @api_stability: experimental
 */
pub fn missing_tool_call_classifier(opts: MissingToolCallOptions = {}) {
  let cfg = opts ?? {}
  if type_of(cfg) != "dict" {
    throw "missing_tool_call_classifier: opts must be a dict or nil; got " + type_of(cfg)
  }
  let enabled = cfg.enabled ?? true
  if type_of(enabled) != "bool" {
    throw "missing_tool_call_classifier: enabled must be a bool"
  }
  let model = __missing_tool_call_string(cfg.model)
  let threshold = __missing_tool_call_threshold(cfg.confidence_threshold)
  let classifier = cfg.classifier
  if classifier != nil && type_of(classifier) != "closure" {
    throw "missing_tool_call_classifier: classifier must be a closure or nil; got " + type_of(classifier)
  }
  return { payload ->
    if !enabled {
      return nil
    }
    let candidate_tools = __missing_tool_call_candidate_tools(payload)
    if len(candidate_tools) == 0 {
      return nil
    }
    let classifier_payload = payload
      + {
      candidate_tools: candidate_tools,
      tool_names: __missing_tool_call_tool_names(candidate_tools),
      confidence_threshold: threshold,
    }
    if classifier != nil {
      let custom = try {
        classifier(classifier_payload)
      }
      if is_err(custom) {
        let err = unwrap_err(custom)
        if error_category(err) == "cancelled" {
          throw err
        }
        return __missing_tool_call_fail_open(err, threshold) + {classifier_kind: "custom"}
      }
      return __missing_tool_call_normalize(unwrap(custom), candidate_tools, threshold)
        + {classifier_kind: "custom"}
    }
    let schema = __missing_tool_call_schema()
    var llm_opts = {
      output_schema: schema,
      max_tokens: cfg.max_tokens ?? 192,
      temperature: cfg.temperature ?? 0.0,
      session_id: payload?.session_id ?? "",
    }
    if model != "" {
      llm_opts = llm_opts + {model: model}
    }
    if cfg.provider != nil {
      llm_opts = llm_opts + {provider: cfg.provider}
    }
    for key in ["top_p", "seed", "reasoning_effort", "timeout"] {
      if cfg[key] != nil {
        llm_opts[key] = cfg[key]
      }
    }
    let checkpoint = agent_typed_output_checkpoint(
      "agent.missing_tool_call",
      __missing_tool_call_prompt(classifier_payload, candidate_tools),
      schema,
      llm_opts,
    )
    if !checkpoint.ok {
      return __missing_tool_call_fail_open(checkpoint.error, threshold)
        + {classifier_kind: "llm", model: model, typed_checkpoint: checkpoint}
    }
    return __missing_tool_call_normalize(checkpoint.data, candidate_tools, threshold)
      + {classifier_kind: "llm", model: model, typed_checkpoint: checkpoint}
  }
}