harn-stdlib 0.8.51

Embedded Harn standard library source catalog
Documentation
import { agent_emit_event } from "std/agent/state"

type AgentLoopCommandAction = "none" | "extend" | "stop"

type AgentLoopCommand = {
  action: AgentLoopCommandAction,
  by: int,
  until: int,
  reason: string,
  status: string,
}

type AgentLoopBudget = {
  mode: string,
  initial: int,
  max: int,
  extend_by: int,
  expose_decisions: bool,
  wall_clock_ms: int?,
  total_cost_usd: float?,
  consecutive_failures: dict?,
}

type AgentLoopBudgetDecision = {
  iteration: int,
  action: string,
  old_limit: int,
  new_limit: int,
  reason: string,
  status: string,
}

type AgentLoopCommandApplication = {
  stop: bool,
  final_status: string,
  stop_reason: string?,
  current_max: int,
  extensions_used: int,
  decisions: list<AgentLoopBudgetDecision>,
}

type AgentLoopState = {
  iteration: int,
  budget: dict,
  turn: dict,
  session: dict,
  completion: dict,
  progress: dict,
}

fn __agent_loop_state(args) -> AgentLoopState {
  let current_limit = args.current_limit
  let iteration = args.iteration
  let remaining = if iteration >= current_limit {
    0
  } else {
    current_limit - iteration
  }
  let missing = args.missing_required_tools
  return {
    iteration: iteration,
    budget: {
      current_limit: current_limit,
      max: args.budget_max,
      remaining: remaining,
      extension_count: args.extensions_used,
      wall_clock_ms: args.wall_clock_ms ?? 0,
      wall_clock_limit_ms: args.wall_clock_limit_ms,
      cost_usd: args.cost_usd ?? 0.0,
      total_cost_limit_usd: args.total_cost_limit_usd,
      consecutive_failures: args.consecutive_failures ?? 0,
      consecutive_failure_limit: args.consecutive_failure_limit,
    },
    turn: {
      tool_call_count: args.turn_tool_count,
      successful_tool_names: args.turn_successful,
      rejected_tool_names: args.turn_rejected,
      text_chars: args.turn_text_chars,
      native_fallback_used: args.turn_native_fallback_used,
    },
    session: {
      successful_tool_names: args.session_successful,
      rejected_tool_names: args.session_rejected,
      required_tools_satisfied: len(missing) == 0,
      required_tools_missing: missing,
    },
    completion: {
      proposed: args.completion_proposed,
      vetoed: args.completion_vetoed,
      verdict: args.completion_verdict,
      feedback: args.completion_feedback,
    },
    progress: {changed: args.progress_changed, summary: args.progress_summary},
  }
}

fn __agent_default_loop_control(state: AgentLoopState) {
  if state.budget.remaining > 0 {
    return nil
  }
  if state.completion.vetoed {
    return {action: "extend", reason: "completion gate vetoed"}
  }
  if !state.session.required_tools_satisfied {
    return {action: "extend", reason: "required tools missing"}
  }
  if state.progress.changed && state.turn.tool_call_count > 0 {
    return {action: "extend", reason: "recent turn made progress"}
  }
  return nil
}

fn __agent_interpret_loop_command(command) -> AgentLoopCommand {
  if command == nil {
    return {action: "none", by: 0, until: 0, reason: "", status: ""}
  }
  if type_of(command) != "dict" {
    throw "agent_loop: loop_control must return nil or a dict; got " + type_of(command)
  }
  let action = command?.action ?? "none"
  if action != "none" && action != "extend" && action != "stop" {
    throw "agent_loop: loop_control action must be \"none\", \"extend\", or \"stop\"; got "
      + to_string(action)
  }
  let by = command?.by ?? 0
  if type_of(by) != "int" {
    throw "agent_loop: loop_control `by` must be an integer; got " + type_of(by)
  }
  let until = command?.until ?? 0
  if type_of(until) != "int" {
    throw "agent_loop: loop_control `until` must be an integer; got " + type_of(until)
  }
  return {action: action, by: by, until: until, reason: command?.reason ?? "", status: command?.status ?? ""}
}

/**
 * agent_loop_control_invoke normalizes a custom or adaptive loop command.
 *
 * @effects: [agent]
 * @allocation: heap
 * @errors: [agent_loop]
 * @api_stability: experimental
 * @example: agent_loop_control_invoke(opts, budget, state)
 */
pub fn agent_loop_control_invoke(opts, budget: AgentLoopBudget, state: AgentLoopState) -> AgentLoopCommand {
  let policy = opts?.loop_control
  if policy != nil {
    return __agent_interpret_loop_command(policy(state))
  }
  if budget.mode == "adaptive" {
    return __agent_interpret_loop_command(__agent_default_loop_control(state))
  }
  return __agent_interpret_loop_command(nil)
}

fn __agent_apply_extension(command: AgentLoopCommand, budget: AgentLoopBudget, current_max: int) {
  let cap = budget.max
  let target = if command.until > 0 {
    command.until
  } else {
    let by = if command.by > 0 {
      command.by
    } else {
      budget.extend_by
    }
    current_max + by
  }
  let bounded = if target > cap {
    cap
  } else {
    target
  }
  if bounded <= current_max {
    return {extended: false, new_limit: current_max, delta: 0}
  }
  return {extended: true, new_limit: bounded, delta: bounded - current_max}
}

fn __agent_record_budget_decision(
  decisions: list<AgentLoopBudgetDecision>,
  iteration: int,
  action: string,
  old_limit: int,
  new_limit: int,
  reason: string,
  status: string,
) -> list<AgentLoopBudgetDecision> {
  return decisions
    .push(
    {
      iteration: iteration,
      action: action,
      old_limit: old_limit,
      new_limit: new_limit,
      reason: reason,
      status: status,
    },
  )
}

fn __agent_progress_summary_text(completion_vetoed: bool, tool_count: int, visible_text: string) -> string {
  if completion_vetoed {
    return "completion gate vetoed"
  }
  if tool_count > 0 {
    return "executed " + to_string(tool_count) + " tool call(s)"
  }
  if trim(visible_text) != "" {
    return "produced visible text"
  }
  return "no progress signal"
}

/**
 * agent_loop_snapshot_state builds the loop-control state callback payload.
 *
 * @effects: []
 * @allocation: heap
 * @errors: []
 * @api_stability: experimental
 * @example: agent_loop_snapshot_state(args)
 */
pub fn agent_loop_snapshot_state(args) -> AgentLoopState {
  let verdict = args.verdict
  let completion_vetoed = verdict != nil && verdict?.vetoed
  let completion_verdict = if verdict == nil {
    nil
  } else {
    verdict?.verdict
  }
  let completion_feedback = if verdict == nil {
    nil
  } else {
    verdict?.feedback
  }
  let visible_text = args.visible_text
  let tool_count = args.tool_count
  let turn_successful = args.turn_successful
  let progress_changed = tool_count > 0 || len(turn_successful) > 0
    || trim(visible_text) != ""
  let progress_summary = __agent_progress_summary_text(completion_vetoed, tool_count, visible_text)
  return __agent_loop_state(
    {
      iteration: args.iteration,
      current_limit: args.current_limit,
      budget_max: args.budget_max,
      extensions_used: args.extensions_used,
      wall_clock_ms: args.wall_clock_ms,
      wall_clock_limit_ms: args.wall_clock_limit_ms,
      cost_usd: args.cost_usd,
      total_cost_limit_usd: args.total_cost_limit_usd,
      consecutive_failures: args.consecutive_failures,
      consecutive_failure_limit: args.consecutive_failure_limit,
      turn_tool_count: tool_count,
      turn_successful: turn_successful,
      turn_rejected: args.turn_rejected,
      turn_text_chars: len(visible_text),
      turn_native_fallback_used: args.turn_native_fallback_used,
      session_successful: args.session_successful,
      session_rejected: args.session_rejected,
      missing_required_tools: args.missing_required_tools,
      completion_proposed: args.completion_proposed,
      completion_vetoed: completion_vetoed,
      completion_verdict: completion_verdict,
      completion_feedback: completion_feedback,
      progress_changed: progress_changed,
      progress_summary: progress_summary,
    },
  )
}

/**
 * agent_loop_apply_command applies a normalized command to the loop budget.
 *
 * @effects: [agent]
 * @allocation: heap
 * @errors: []
 * @api_stability: experimental
 * @example: agent_loop_apply_command(state)
 */
pub fn agent_loop_apply_command(state) -> AgentLoopCommandApplication {
  let command = state.command
  let session_id = state.session_id
  let iteration = state.iteration
  let current_max = state.current_max
  let budget = state.budget
  if command.action == "stop" {
    let stop_status = if command.status != "" {
      command.status
    } else {
      "stopped"
    }
    let stop_reason = if command.reason != "" {
      command.reason
    } else {
      "loop_control"
    }
    let decisions = __agent_record_budget_decision(
      state.decisions,
      iteration,
      "stop",
      current_max,
      current_max,
      command.reason,
      stop_status,
    )
    agent_emit_event(
      session_id,
      "loop_control_decision",
      {
        iteration: iteration,
        action: "stop",
        old_limit: current_max,
        new_limit: current_max,
        reason: command.reason,
        status: stop_status,
      },
    )
    return {
      stop: true,
      final_status: stop_status,
      stop_reason: stop_reason,
      current_max: current_max,
      extensions_used: state.extensions_used,
      decisions: decisions,
    }
  }
  if command.action == "extend" {
    let applied = __agent_apply_extension(command, budget, current_max)
    if applied.extended {
      let old_limit = current_max
      let new_limit = applied.new_limit
      let extensions_used = state.extensions_used + 1
      let decisions = __agent_record_budget_decision(
        state.decisions,
        iteration,
        "extend",
        old_limit,
        new_limit,
        command.reason,
        "",
      )
      agent_emit_event(
        session_id,
        "loop_control_decision",
        {
          iteration: iteration,
          action: "extend",
          old_limit: old_limit,
          new_limit: new_limit,
          reason: command.reason,
          status: "",
        },
      )
      return {
        stop: false,
        final_status: "",
        stop_reason: nil,
        current_max: new_limit,
        extensions_used: extensions_used,
        decisions: decisions,
      }
    }
  }
  return {
    stop: false,
    final_status: "",
    stop_reason: nil,
    current_max: current_max,
    extensions_used: state.extensions_used,
    decisions: state.decisions,
  }
}