harn-stdlib 0.8.163

Embedded Harn standard library source catalog
Documentation
/**
 * @harn-entrypoint-category workflow.stdlib
 *
 * std/workflow/patterns — small, deterministic workflow recipes. These are
 * builders and control-flow helpers, not another workflow runtime and not a
 * provider client.
 */
fn __patterns_dict(value) {
  if type_of(value) == "dict" {
    return value
  }
  return {}
}

fn __patterns_list(value) {
  if type_of(value) == "list" {
    return value
  }
  return []
}

fn __patterns_is_callable(value) -> bool {
  let kind = type_of(value)
  return kind == "function" || kind == "closure" || kind == "fn"
}

fn __patterns_non_empty_string(value) {
  if type_of(value) != "string" {
    return nil
  }
  let trimmed = trim(value)
  if trimmed == "" {
    return nil
  }
  return trimmed
}

fn __patterns_model_policy(config, key) {
  let cfg = __patterns_dict(config)
  let specific = __patterns_dict(cfg?[key])
  let shared = __patterns_dict(cfg?.model_policy)
  return shared + specific
}

fn __patterns_context_policy(config, key) {
  let cfg = __patterns_dict(config)
  let specific = __patterns_dict(cfg?[key])
  let shared = __patterns_dict(cfg?.context_policy)
  return shared + specific
}

fn __patterns_output_contract(default_kind, override_cfg) {
  let cfg = __patterns_dict(override_cfg)
  if cfg?.output_contract != nil {
    return cfg.output_contract
  }
  return {output_kinds: [default_kind]}
}

fn __patterns_stage_node(config, key, defaults) {
  let cfg = __patterns_dict(config)
  let explicit_node = cfg?[key + "_node"]
  let override_cfg = if explicit_node != nil {
    __patterns_dict(explicit_node)
  } else if key == "verify" {
    {}
  } else {
    __patterns_dict(cfg?[key])
  }
  var node = defaults + override_cfg
  if node?.model_policy == nil {
    node = node + {model_policy: __patterns_model_policy(cfg, key + "_model_policy")}
  }
  if node?.context_policy == nil {
    node = node + {context_policy: __patterns_context_policy(cfg, key + "_context_policy")}
  }
  return node
}

/**
 * Build a one-stage graph that runs an acting stage and then a verify stage.
 *
 * @effects: []
 * @errors: []
 * @api_stability: experimental
 */
pub fn workflow_self_verifying_graph(config = nil) {
  let cfg = __patterns_dict(config)
  let act_id = __patterns_non_empty_string(cfg?.act_id) ?? "act"
  let verify_id = __patterns_non_empty_string(cfg?.verify_id) ?? "verify"
  let act = __patterns_stage_node(
    cfg,
    "act",
    {
      kind: "stage",
      mode: cfg?.mode ?? "agent",
      model_policy: cfg?.model_policy ?? {},
      output_contract: __patterns_output_contract(cfg?.act_output_kind ?? "draft", cfg?.act),
    },
  )
  let verify = __patterns_stage_node(
    cfg,
    "verify",
    {
      kind: "verify",
      mode: cfg?.verify_mode ?? "command",
      verify: cfg?.verify ?? {},
      output_contract: __patterns_output_contract(cfg?.verify_output_kind ?? "verification_result", cfg?.verify_node),
    },
  )
  return workflow_graph(
    {
      name: cfg?.name ?? "self_verifying",
      entry: act_id,
      nodes: {[act_id]: act, [verify_id]: verify},
      edges: [{from: act_id, to: verify_id}],
    },
  )
}

/**
 * Build the common implement -> verify -> repair -> verify loop.
 *
 * The graph is explicit: verification failure follows branch `"failed"` into
 * the repair stage, then repair returns to verify. Callers bound total work
 * with workflow_execute({max_steps}) and per-agent `iteration_budget`.
 *
 * @effects: []
 * @errors: []
 * @api_stability: experimental
 */
pub fn workflow_command_verify_graph(config = nil) {
  let cfg = __patterns_dict(config)
  let implement_id = __patterns_non_empty_string(cfg?.implement_id) ?? "implement"
  let verify_id = __patterns_non_empty_string(cfg?.verify_id) ?? "verify"
  let repair_id = __patterns_non_empty_string(cfg?.repair_id) ?? "repair"
  let implement = __patterns_stage_node(
    cfg,
    "implement",
    {
      kind: "stage",
      mode: cfg?.mode ?? "agent",
      model_policy: cfg?.model_policy ?? {},
      output_contract: __patterns_output_contract(cfg?.implement_output_kind ?? "draft", cfg?.implement),
    },
  )
  let verify = __patterns_stage_node(
    cfg,
    "verify",
    {
      kind: "verify",
      mode: cfg?.verify_mode ?? "command",
      verify: cfg?.verify ?? {},
      output_contract: __patterns_output_contract(cfg?.verify_output_kind ?? "verification_result", cfg?.verify_node),
    },
  )
  let repair_policy = __patterns_dict(cfg?.repair_model_policy)
  let repair = __patterns_stage_node(
    cfg,
    "repair",
    {
      kind: "stage",
      mode: cfg?.repair_mode ?? "agent",
      model_policy: __patterns_dict(cfg?.model_policy) + repair_policy,
      metadata: {repair: true, verifies: verify_id, repairs: implement_id},
      output_contract: __patterns_output_contract(cfg?.repair_output_kind ?? "patch", cfg?.repair),
    },
  )
  return workflow_graph(
    {
      name: cfg?.name ?? "command_verify",
      entry: implement_id,
      nodes: {[implement_id]: implement, [verify_id]: verify, [repair_id]: repair},
      edges: [
        {from: implement_id, to: verify_id},
        {from: verify_id, to: repair_id, branch: "failed"},
        {from: repair_id, to: verify_id},
      ],
    },
  )
}

/**
 * Build a graph with only a verification stage. Useful for replaying a
 * deterministic verifier against supplied artifacts.
 *
 * @effects: []
 * @errors: []
 * @api_stability: experimental
 */
pub fn workflow_verification_only_graph(config = nil) {
  let cfg = __patterns_dict(config)
  let verify_id = __patterns_non_empty_string(cfg?.verify_id) ?? "verify"
  let verify = __patterns_stage_node(
    cfg,
    "verify",
    {
      kind: "verify",
      mode: cfg?.verify_mode ?? "command",
      verify: cfg?.verify ?? {},
      output_contract: __patterns_output_contract(cfg?.verify_output_kind ?? "verification_result", cfg?.verify_node),
    },
  )
  return workflow_graph(
    {name: cfg?.name ?? "verification_only", entry: verify_id, nodes: {[verify_id]: verify}, edges: []},
  )
}

fn __failover_disposition(value, fallback_action) {
  if value == nil {
    return {action: fallback_action}
  }
  if type_of(value) == "string" {
    return {action: value}
  }
  if type_of(value) == "dict" {
    return value + {action: value?.action ?? value?.decision ?? value?.status ?? fallback_action}
  }
  return {action: fallback_action, raw: value}
}

fn __failover_route_id(route, index) -> string {
  let explicit = __patterns_non_empty_string(route?.id ?? route?.name ?? route?.model ?? route?.provider)
  if explicit != nil {
    return explicit
  }
  return "route-" + to_string(index)
}

fn __failover_evaluate(config, route, ctx) {
  let evaluator = config?.evaluate
  if __patterns_is_callable(evaluator) {
    return __failover_disposition(evaluator(route, ctx), "call")
  }
  return {action: "call"}
}

fn __failover_classify(config, route, result, ctx) {
  let classifier = config?.classify
  if __patterns_is_callable(classifier) {
    return __failover_disposition(classifier(route, result, ctx), "continue")
  }
  if result?.ok ?? false {
    return {action: "success"}
  }
  return {action: "continue"}
}

fn __failover_terminal_status(decision) -> string {
  let action = to_string(decision?.action ?? "")
  if action == "success" {
    return "success"
  }
  if action == "break" || action == "terminal" {
    return to_string(decision?.status ?? decision?.reason ?? "terminal")
  }
  return action
}

/**
 * Run a deterministic failover loop over opaque route handles.
 *
 * Required config: `{routes, call}`. Optional callbacks:
 *   - `evaluate(route, ctx) -> {action: "call"|"skip", reason?}`
 *   - `classify(route, result, ctx) -> {action: "success"|"continue"|"break"|"terminal", reason?}`
 *
 * Harn owns attempt ordering and receipts. The caller owns provider HTTP,
 * credentials, endpoint policy, billing, and any route-handle semantics.
 *
 * @effects: []
 * @errors: ["workflow_failover: call must be callable"]
 * @api_stability: experimental
 */
pub fn workflow_failover(config = nil) {
  let cfg = __patterns_dict(config)
  let routes = __patterns_list(cfg?.routes)
  let caller = cfg?.call
  if !__patterns_is_callable(caller) {
    throw "workflow_failover: call must be callable"
  }
  let max_attempts = to_int(cfg?.max_attempts ?? len(routes))
  let base_ctx = __patterns_dict(cfg?.context)
  var ctx = base_ctx + {attempts: []}
  var attempts = []
  var call_attempts = 0
  var index = 0
  for route in routes {
    let route_id = __failover_route_id(route, index)
    let evaluated = __failover_evaluate(cfg, route, ctx)
    if evaluated.action == "skip" {
      let attempt = {
        index: index,
        route_id: route_id,
        route: route,
        status: "skipped",
        decision: "skip",
        reason: evaluated?.reason,
      }
      attempts = attempts + [attempt]
      ctx = ctx + {attempts: attempts, last_attempt: attempt}
      index = index + 1
      continue
    }
    if call_attempts >= max_attempts {
      break
    }
    call_attempts = call_attempts + 1
    let result = caller(route, ctx)
    let decision = __failover_classify(cfg, route, result, ctx + {result: result})
    let action = to_string(decision?.action ?? "continue")
    let attempt = {
      index: index,
      route_id: route_id,
      route: route,
      status: if action == "success" {
        "succeeded"
      } else {
        "failed"
      },
      decision: action,
      reason: decision?.reason,
      result: result,
    }
    attempts = attempts + [attempt]
    ctx = ctx + {attempts: attempts, last_attempt: attempt}
    if action == "success" {
      return {
        ok: true,
        status: "success",
        selected: route,
        selected_route_id: route_id,
        value: result,
        attempts: attempts,
        call_attempts: call_attempts,
      }
    }
    if action == "break" || action == "terminal" {
      return {
        ok: false,
        status: __failover_terminal_status(decision),
        selected: route,
        selected_route_id: route_id,
        value: result,
        attempts: attempts,
        call_attempts: call_attempts,
      }
    }
    index = index + 1
  }
  return {
    ok: false,
    status: "exhausted",
    selected: nil,
    selected_route_id: nil,
    value: nil,
    attempts: attempts,
    call_attempts: call_attempts,
  }
}