harn-stdlib 0.10.25

Embedded Harn standard library source catalog
Documentation
import {
  MonologueActuationConfig,
  MonologueActuationDecision,
  MonologueActuationState,
} from "std/agent/monologue_actuation_types"
import { agent_has_tools, agent_render_tool_call_prefix } from "std/agent/preflight"
import { agent_emit_event } from "std/agent/state"
import { agent_tool_resolve_workspace_write_name } from "std/agent/tool_annotations"
import { llm_has_explicit_routing_owner } from "std/llm/options"

/**
 * Bounded, opt-in recovery for tool-less action narration or a repeated
 * read-only loop that never commits to an available workspace-write tool. This
 * module owns the small state machine and transport-independent receipt; the
 * agent loop attaches the returned prefill to its next main request only.
 */
/**
 * Normalize the nested `stall_diagnostics.monologue_actuation` input. It is
 * intentionally default-off, and a bool is shorthand for `{enabled: bool}`.
 *
 * @effects: []
 * @errors: [agent_loop]
 * @api_stability: experimental
 */
pub fn agent_monologue_actuation_config(value = nil) -> MonologueActuationConfig {
  const defaults: MonologueActuationConfig = {enabled: false, max_firings: 1, read_only_turns: 4, write_tool: ""}
  if value == nil {
    return defaults
  }
  if type_of(value) == "bool" {
    return defaults + {enabled: value}
  }
  if type_of(value) != "dict" {
    throw "agent_loop: stall_diagnostics.monologue_actuation must be a bool, dict, or nil; got "
      + type_of(value)
  }
  const enabled = value?.enabled
  if enabled != nil && type_of(enabled) != "bool" {
    throw "agent_loop: stall_diagnostics.monologue_actuation.enabled must be a bool; got "
      + type_of(enabled)
  }
  const max_firings = value?.max_firings
  if max_firings != nil && (type_of(max_firings) != "int" || max_firings < 1) {
    throw "agent_loop: stall_diagnostics.monologue_actuation.max_firings must be a positive int; got "
      + type_of(max_firings)
  }
  const read_only_turns = value?.read_only_turns
  if read_only_turns != nil && (type_of(read_only_turns) != "int" || read_only_turns < 1) {
    throw "agent_loop: stall_diagnostics.monologue_actuation.read_only_turns must be a positive int; got "
      + type_of(read_only_turns)
  }
  const write_tool = value?.write_tool
  if write_tool != nil && type_of(write_tool) != "string" {
    throw "agent_loop: stall_diagnostics.monologue_actuation.write_tool must be a string; got "
      + type_of(write_tool)
  }
  return {
    enabled: enabled ?? true,
    max_firings: max_firings ?? defaults.max_firings,
    read_only_turns: read_only_turns ?? defaults.read_only_turns,
    write_tool: trim(write_tool ?? ""),
  }
}

/**
 * Return fresh state for one agent run.
 *
 * @effects: []
 * @errors: []
 * @api_stability: experimental
 */
pub fn agent_monologue_actuation_initial_state() -> MonologueActuationState {
  return {pending: false, pending_trigger: "", firing_count: 0, read_only_streak: 0}
}

/**
 * Fold a pre-dispatch stall warning into the actuation trigger. Completed tool
 * dispatches advance their own streak through
 * `agent_monologue_actuation_observe_completed_dispatch`.
 *
 * @effects: []
 * @errors: []
 * @api_stability: experimental
 */
pub fn agent_monologue_actuation_observe(
  state: MonologueActuationState,
  config: MonologueActuationConfig,
  warning_pattern: string,
) -> MonologueActuationState {
  if !config.enabled {
    return state
  }
  if warning_pattern == "no_progress_monologue" {
    return state + {pending: true, pending_trigger: warning_pattern}
  }
  return state
}

/**
 * Advance the read-only streak from one completed dispatch. The loop supplies
 * a product-error-aware all-success predicate and the streak captured before
 * the current turn was reset to a non-success default. Failed, mixed, empty,
 * and undispatched turns therefore cannot receive grounding credit.
 *
 * @effects: []
 * @errors: []
 * @api_stability: experimental
 */
pub fn agent_monologue_actuation_observe_completed_dispatch(
  state: MonologueActuationState,
  config: MonologueActuationConfig,
  prior_read_only_streak: int,
  successful_read_only: bool,
) -> MonologueActuationState {
  if !config.enabled {
    return state
  }
  if !successful_read_only {
    return state + {read_only_streak: 0}
  }
  const next = state + {read_only_streak: prior_read_only_streak + 1}
  if next.read_only_streak < config.read_only_turns {
    return next
  }
  return next + {pending: true, pending_trigger: "read_only_streak"}
}

fn __agent_monologue_actuation_route_suppression(options) -> string {
  if options?._llm_caller != nil
    && !(options?._llm_caller_transport?.forwards_assistant_prefill ?? false) {
    return "external_llm_caller_unsupported"
  }
  if llm_has_explicit_routing_owner(options) || options?.equivalent_failover != nil {
    return "multi_route_prefill_unsupported"
  }
  const capabilities = try {
    provider_capabilities(options?.provider ?? "auto", options?.model ?? "")
  }
  if is_err(capabilities) || type_of(unwrap(capabilities)) != "dict" {
    return "assistant_prefill_unsupported"
  }
  if !(unwrap(capabilities)?.supports_assistant_prefill ?? false) {
    return "assistant_prefill_unsupported"
  }
  return ""
}

fn __agent_monologue_actuation_receipt(
  session_id: string,
  iteration: int,
  decision: MonologueActuationDecision,
  tool_format: string,
) {
  agent_emit_event(
    session_id,
    "typed_checkpoint",
    {
      schema: "harn.agent_loop_monologue_actuation.v1",
      kind: "monologue_actuation",
      iteration: iteration,
      outcome: if decision.applied {
        "applied"
      } else {
        "suppressed"
      },
      reason: decision.reason,
      firing_count: decision.firing_count,
      max_firings: decision.max_firings,
      tool_format: tool_format,
      write_tool: decision.write_tool,
      trigger: decision.trigger,
    },
  )
}

/**
 * Consume a pending recovery before auxiliary work runs. Only the current
 * main route may receive the returned prefill; native formats, missing tools,
 * unsupported providers, and exhausted budgets produce typed suppression
 * receipts instead.
 *
 * @effects: [agent]
 * @errors: []
 * @api_stability: experimental
 */
pub fn agent_monologue_actuation_take(
  session_id: string,
  iteration: int,
  state: MonologueActuationState,
  config: MonologueActuationConfig,
  has_tools: bool,
  llm_options,
  prefill_available: bool,
  paradigm = nil,
  commit_tool_name: string = "",
) -> MonologueActuationDecision {
  if !state.pending {
    return {
      state: state,
      applied: false,
      reason: "not_pending",
      prefill: "",
      write_tool: commit_tool_name,
      trigger: "",
      firing_count: state.firing_count,
      max_firings: config.max_firings,
    }
  }
  const trigger = state.pending_trigger
  const cleared = state + {pending: false, pending_trigger: "", read_only_streak: 0}
  const tool_format = to_string(paradigm?.kind ?? "")
  const call_open = to_string(paradigm?.call_open ?? "")
  const actuation_prefill = if trigger == "read_only_streak" {
    agent_render_tool_call_prefix(commit_tool_name, llm_options)
  } else {
    call_open
  }
  const route_suppression = __agent_monologue_actuation_route_suppression(llm_options)
  const decision = if !config.enabled {
    {
      state: cleared,
      applied: false,
      reason: "disabled",
      prefill: "",
      trigger: trigger,
      firing_count: cleared.firing_count,
      max_firings: config.max_firings,
    }
  } else if cleared.firing_count >= config.max_firings {
    {
      state: cleared,
      applied: false,
      reason: "max_firings_exhausted",
      prefill: "",
      trigger: trigger,
      firing_count: cleared.firing_count,
      max_firings: config.max_firings,
    }
  } else if !has_tools {
    {
      state: cleared,
      applied: false,
      reason: "no_tools",
      prefill: "",
      trigger: trigger,
      firing_count: cleared.firing_count,
      max_firings: config.max_firings,
    }
  } else if !prefill_available {
    {
      state: cleared,
      applied: false,
      reason: "prefill_already_present",
      prefill: "",
      trigger: trigger,
      firing_count: cleared.firing_count,
      max_firings: config.max_firings,
    }
  } else if trigger == "read_only_streak" && commit_tool_name == "" {
    {
      state: cleared,
      applied: false,
      reason: "commit_tool_unavailable",
      prefill: "",
      trigger: trigger,
      firing_count: cleared.firing_count,
      max_firings: config.max_firings,
    }
  } else if call_open == "" {
    {
      state: cleared,
      applied: false,
      reason: "tool_call_opener_unavailable",
      prefill: "",
      trigger: trigger,
      firing_count: cleared.firing_count,
      max_firings: config.max_firings,
    }
  } else if route_suppression != "" {
    {
      state: cleared,
      applied: false,
      reason: route_suppression,
      prefill: "",
      trigger: trigger,
      firing_count: cleared.firing_count,
      max_firings: config.max_firings,
    }
  } else {
    const applied_state = cleared + {firing_count: cleared.firing_count + 1}
    {
      state: applied_state,
      applied: true,
      reason: "eligible_text_tool_paradigm",
      prefill: actuation_prefill,
      trigger: trigger,
      firing_count: applied_state.firing_count,
      max_firings: config.max_firings,
    }
  }
  const attributed_decision: MonologueActuationDecision = decision + {write_tool: commit_tool_name}
  __agent_monologue_actuation_receipt(session_id, iteration, attributed_decision, tool_format)
  return attributed_decision
}

/**
 * Resolve host policy and consume a pending recovery for one main agent turn.
 * Keeping this projection here prevents the oversized loop from duplicating
 * config normalization and write-tool validation.
 *
 * @effects: [agent]
 * @errors: [agent_loop]
 * @api_stability: experimental
 */
pub fn agent_monologue_actuation_take_for_turn(
  session_id: string,
  iteration: int,
  state: MonologueActuationState,
  turn_options,
  llm_options,
  prefill_available: bool,
  paradigm = nil,
) -> MonologueActuationDecision {
  const config = agent_monologue_actuation_config(turn_options?.stall_diagnostics?.monologue_actuation)
  return agent_monologue_actuation_take(
    session_id,
    iteration,
    state,
    config,
    agent_has_tools(turn_options),
    llm_options,
    prefill_available,
    paradigm,
    agent_tool_resolve_workspace_write_name(
      turn_options?.tools,
      turn_options?.policy,
      config.write_tool,
    ),
  )
}