harn-stdlib 0.10.165

Embedded Harn standard library source catalog
Documentation
import { AgentLoopCancelledFailure } from "std/agent/failure_types"
pub import { MissingToolCallClassifier, MissingToolCallVerdict } from "std/agent/options_types"
import { MissingToolCallRecoveryOptions } from "std/agent/options_types"
import { EvaluationOutcome, EvaluationPolicy, boolean, choice } from "std/predicate"

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

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

type MissingToolCallRoute = {provider: string, model: string}

type MissingToolCallCapabilities = {
  decision_protocol: string?,
  reasoning_effort_levels: list<string>?,
}

fn __missing_tool_call_policy(
  llm: HarnessLlm,
  cfg: MissingToolCallRecoveryOptions,
) -> EvaluationPolicy {
  if cfg.evaluation != nil {
    return cfg.evaluation + {threshold: 0.0}
  }
  const route: MissingToolCallRoute = schema_expect(
    llm.resolved_options({provider: cfg.provider ?? "", model: cfg.model ?? ""}),
    schema_of(MissingToolCallRoute),
  )
  const capabilities: MissingToolCallCapabilities = schema_expect(
    llm.provider_capabilities(route.provider, route.model),
    schema_of(MissingToolCallCapabilities),
  )
  const native = capabilities.decision_protocol != nil
    && capabilities.decision_protocol != "structured_llm"
  let policy: EvaluationPolicy = {
    backend: if native {
      "native_decision"
    } else {
      "structured_llm"
    },
    provider: route.provider,
    model: route.model,
    threshold: 0.0,
  }
  if !native {
    const levels = capabilities.reasoning_effort_levels ?? []
    // The catalog lists supported effort levels from lowest to highest.
    // Keep this bounded classifier at the lowest declared effort instead of
    // inventing an off switch that the route may reject.
    policy = policy + {max_tokens: 192}
    if len(levels) > 0 {
      policy = policy + {effort: levels[0]}
    }
  }
  if cfg.effort != nil {
    policy = policy + {effort: cfg.effort}
  }
  if cfg.temperature != nil {
    policy = policy + {temperature: cfg.temperature}
  }
  if cfg.max_tokens != nil {
    policy = policy + {max_tokens: cfg.max_tokens}
  }
  if cfg.top_p != nil {
    policy = policy + {top_p: cfg.top_p}
  }
  if cfg.seed != nil {
    policy = policy + {seed: cfg.seed}
  }
  if cfg.timeout_ms != nil {
    policy = policy + {timeout_ms: cfg.timeout_ms}
  }
  return policy
}

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

fn __missing_tool_call_float(value: float?, fallback: float) {
  const parsed = to_float(value ?? fallback)
  if parsed == nil {
    return fallback
  }
  if !(parsed >= 0.0 && parsed <= 1.0) {
    return fallback
  }
  return parsed
}

fn __missing_tool_call_config_error(code: string, message: string, fields: dict? = {}) {
  return {
    error: code,
    category: "invalid_config",
    component: "missing_tool_call_classifier",
    message: message,
  }
    + (fields ?? {})
}

fn __missing_tool_call_threshold(value: float?) -> float {
  const threshold = to_float(value ?? 0.65)
  if threshold == nil || !(threshold >= 0.0 && threshold <= 1.0) {
    throw __missing_tool_call_config_error(
      "missing_tool_call.invalid_confidence_threshold",
      "confidence_threshold must be between 0.0 and 1.0",
      {field: "confidence_threshold", expected: "0.0..1.0", actual: value, min: 0.0, max: 1.0},
    )
  }
  return threshold
}

fn __missing_tool_call_candidate_tool_entries(payload: dict) {
  const 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: dict) -> list<MissingToolCallTool> {
  let out = []
  let names = []
  for entry in __missing_tool_call_candidate_tool_entries(payload) {
    const name = __missing_tool_call_string(entry?.name ?? entry?.function?.name)
    if name == "" || contains(names, name) {
      continue
    }
    names = names.appending(name)
    out = out.appending(
      {
        name: name,
        description: __missing_tool_call_string(entry?.description ?? entry?.function?.description),
      },
    )
  }
  for name in payload?.tool_names ?? [] {
    const text = __missing_tool_call_string(name)
    if text != "" && !contains(names, text) {
      names = names.appending(text)
      out = out.appending({name: text, description: ""})
    }
  }
  return out
}

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

fn __missing_tool_call_tool_name(value: any, candidate_tools: list<MissingToolCallTool>) -> string {
  const 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_action(value: any) -> MissingToolCallAction {
  const 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: any,
  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",
    }
  }
  const original_action = __missing_tool_call_action(raw?.action)
  const confidence = __missing_tool_call_float(raw?.confidence, 0.0)
  const tool_name = __missing_tool_call_tool_name(raw?.tool_name ?? raw?.tool, candidate_tools)
  const 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"
  }
  const 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: any, 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 evaluates intent and target in
 * one request, using `evaluation` when configured and the active structured
 * model otherwise. Refusal fails open to `ambiguous`; cancellation propagates.
 *
 * @effects: [llm.call]
 * @errors: ["invalid classifier configuration", "cancelled"]
 * @api_stability: experimental
 */
pub fn missing_tool_call_classifier(
  harness: Harness,
  opts: MissingToolCallRecoveryOptions? = nil,
) -> MissingToolCallClassifier {
  const cfg = opts ?? {}
  if type_of(cfg) != "dict" {
    throw __missing_tool_call_config_error(
      "missing_tool_call.invalid_options",
      "opts must be a dict or nil; got " + type_of(cfg),
      {field: "opts", expected: "dict", actual_type: type_of(cfg)},
    )
  }
  const enabled = cfg.enabled ?? true
  if type_of(enabled) != "bool" {
    throw __missing_tool_call_config_error(
      "missing_tool_call.invalid_enabled",
      "enabled must be a bool",
      {field: "enabled", expected: "bool", actual_type: type_of(enabled)},
    )
  }
  const model = __missing_tool_call_string(cfg.model)
  const threshold = __missing_tool_call_threshold(cfg.confidence_threshold)
  const classifier = cfg.classifier
  if classifier != nil && type_of(classifier) != "closure" {
    throw __missing_tool_call_config_error(
      "missing_tool_call.invalid_classifier",
      "classifier must be a closure or nil; got " + type_of(classifier),
      {field: "classifier", expected: "closure|nil", actual_type: type_of(classifier)},
    )
  }
  return { payload ->
    if !enabled {
      return nil
    }
    const candidate_tools = __missing_tool_call_candidate_tools(payload)
    if len(candidate_tools) == 0 {
      return nil
    }
    const classifier_payload = payload
      + {
        candidate_tools: candidate_tools,
        tool_names: __missing_tool_call_tool_names(candidate_tools),
        confidence_threshold: threshold,
      }
    if classifier != nil {
      const custom = try {
        classifier(classifier_payload)
      }
      if is_err(custom) {
        const 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"}
    }
    const resolved = try {
      __missing_tool_call_policy(harness.llm, cfg)
    }
    if is_err(resolved) {
      return __missing_tool_call_fail_open(unwrap_err(resolved), threshold)
        + {classifier_kind: "llm", model: model}
    }
    const policy: EvaluationPolicy = unwrap(resolved)
    const state: {candidate_tools: list<MissingToolCallTool>, text: string} = {
      candidate_tools: candidate_tools,
      text: __missing_tool_call_string(payload?.text ?? payload?.visible_text),
    }
    let criteria: dict<string, string> = {}
    for candidate in candidate_tools {
      criteria[candidate.name] = candidate.description ?? ""
    }
    let outcome: EvaluationOutcome? = nil
    try {
      outcome = harness.llm.evaluate_request(
        "agent.missing_tool_call.v1",
        state,
        {
          intended: boolean(
            harness.fs.render_prompt("std/llm/prompts/missing_tool_call_intent.harn.prompt", {}),
          ),
          tool: choice(
            harness.fs.render_prompt("std/llm/prompts/missing_tool_call_target.harn.prompt", {}),
            criteria,
          ),
        },
        policy,
      )
    } catch (err) {
      if error_category(err) == "cancelled" {
        throw err
      }
      return __missing_tool_call_fail_open(err, threshold)
        + {classifier_kind: "llm", model: policy.model}
    }
    if outcome == nil {
      return __missing_tool_call_fail_open("evaluation returned no outcome", threshold)
        + {classifier_kind: "llm", model: policy.model}
    }
    if outcome.kind == "cancelled" {
      const failure: AgentLoopCancelledFailure = {
        category: "cancelled",
        message: "missing tool call evaluation cancelled",
      }
      throw failure
    }
    const metadata = {classifier_kind: "llm", model: policy.model, decision: outcome}
    if outcome.kind != "answered" {
      return __missing_tool_call_fail_open(outcome, threshold) + metadata
    }
    const intended = outcome.value.intended
    const target = outcome.value.tool
    if intended.kind != "boolean" || target.kind != "choice" {
      return __missing_tool_call_fail_open("invalid answer kinds", threshold) + metadata
    }
    return __missing_tool_call_normalize(
      {
        action: if intended.verdict {
          "tool_call_intended"
        } else {
          "no_tool_call_intended"
        },
        tool_name: if intended.verdict {
          target.choice
        } else {
          ""
        },
        confidence: if intended.verdict {
          min(intended.confidence, target.confidence)
        } else {
          intended.confidence
        },
        evidence: intended.evidence,
      },
      candidate_tools,
      threshold,
    )
      + metadata
  }
}