Pre-release Harn is pre-1.0 — the language, standard library, and CLI may change between releases. See the release notes

Migrating to 0.10

Harn 0.10 removes long-deprecated resilience and composition surfaces. There are no compatibility shims. harn check and the runtime reject removed LLM option names and report the canonical replacement.

LLM call options

The llm_call family, streams, and agent_loop dispatch now share one option registry. Update option dicts before upgrading the runtime; stale computed dicts fail at the call boundary instead of losing fields during projection.

Update a call

  1. Replace each removed top-level key using the table below.
  2. Move provider-specific knobs below provider_options.<provider>.
  3. Run harn check <path> to find removed names in literals.
  4. Exercise any computed option builders. Runtime validation covers shapes the checker cannot know statically.
// Before
const old = llm_call(prompt, nil, {
  output_schema: schema,
  output_validation: "error",
  reasoning_effort: "high",
  system_prefix: "Be precise.",
  timeout: 120,
  ollama: {num_ctx: 32768},
})

// After
import {system_before} from "std/llm/prompts"

const current = llm_call(prompt, nil, {
  output: {schema: schema, validation: "error"},
  effort: "high",
  system: [system_before("Be precise.")],
  timeout_ms: 120000,
  provider_options: {ollama: {num_ctx: 32768}},
})

Replacement table

RemovedReplacement
schema, json_schema, output_schemaoutput: schema or output: {schema, ...}
response_format, output_formatoutput: "json" or output: {schema, ...}
output_validationoutput: {schema, validation: ...}
schema_stream_abortoutput: {schema, stream_abort: ...}
llm_repairrepair
system_preamble, system_prefix, system_contextsystem: [{content, position: "before"}]
system_appendix, system_suffixsystem: [{content, position: "after"}]
system_prompt_partssystem
caps, project_context_profilecapabilities, context_profile
api, roleapi_mode, model_role
prefer, fallback_strategy, strategyroute_policy
model_laddermodels for inline steps; ladder for a catalog name
budget_usdbudget: {max_cost_usd: ...}
reasoning_efforteffort
thinking_policy, problem_scale, task_kind, taskreasoning_policy, reasoning_scale, reasoning_task
hosted_toolsprovider_tools
response_store, responses_storestore
timeout, idle_timeouttimeout_ms, idle_timeout_ms (milliseconds)
fastspeed: "fast"
top-level provider names such as openai or ollamaprovider_options: {openai: {...}}
transcriptopen/resume a session and pass session_id

The complete live surface is in the llm_call reference.

llm_retries / llm_backoff_ms (removed)

The per-call transient-retry options on llm_call-family surfaces and agent_loop are gone. llm_call and agent_loop are now fail-fast on transient provider errors (429 / 5xx / connection); retry policy is composed on the caller seam with with_retry from std/llm/handlers.

Mind the off-by-one: llm_retries: K counted retries after the first attempt, while with_retry's max_attempts counts total attempts, so llm_retries: Kwith_retry(..., {max_attempts: K + 1}).

Before (0.9)

const r = llm_call("prompt", nil, {llm_retries: 2, llm_backoff_ms: 500})

After (0.10)

import {default_llm_caller} from "std/llm/caller"
import {with_retry} from "std/llm/handlers"

const caller = with_retry(default_llm_caller(), {max_attempts: 3, base_ms: 500})
const envelope = caller({prompt: "prompt", system: nil, opts: {}})

For agent_loop, wire the composed caller into the llm_caller: seam:

const result = agent_loop(task, system, {
  loop_until_done: true,
  llm_caller: with_retry(default_llm_caller(), {max_attempts: 3}),
})

Behavior changes that ride along:

  • agent_loop profiles no longer default to two transient retries. The pre-0.10 profiles injected llm_retries: 2; loops that relied on that implicit resilience must opt in via llm_caller: + with_retry — or build their options with agent_preset(kind, ...), which bakes the equivalent bounded transport retry (with_retry, max_attempts: 3) onto the effective llm_caller: by default (retry: false opts out).
  • The zero-token empty-completion retry is now a fixed built-in: one silent retry per call for provider-shaped routes (none for the mock provider, whose scripted turns must stay deterministic). It is no longer widened by llm_retries.
  • The proactive per-route rate limiter, shared 429/overload cooldowns, the network circuit breaker, and the one-shot tool-channel / stream-transport degrades are unchanged — they never depended on the removed options.

std/agent/stack (removed)

The std/agent/stack module and its composition bundle are gone: agent_stack, agent_llm_caller, agent_tool_stack, agent_stack_audit_line, and agent_stack_model_policy no longer exist. Compose the pieces directly:

  • Model/route resolution: agent_model_options and agent_sanitize_model_options survive and now live in std/agent/options (same signatures; the audit.kind label changed from agent_stack.model_options to agent_model_options). They are ordinary module exports now, not ambient builtins — add import {agent_model_options} from "std/agent/options".
  • LLM middleware: compose / with_retry / with_logging / with_cache / with_budget from std/llm/handlers on the llm_caller: seam.
  • Tool middleware: with_required_reason, compose_tool_callers, and tools_use_middleware from std/llm/tool_middleware on the tool_caller: seam.
  • agent_stack_model_policy's projection is a pick_keys(options, [...], {drop_nil: true}) call with the stable model-policy keys.

Before (0.9)

import {agent_stack} from "std/agent/stack"

const stack = agent_stack({role: "planner", retry: {max_attempts: 3}, required_reason: true})
const result = agent_loop(task, system, stack.options + {loop_until_done: true})

After (0.10)

import {agent_model_options} from "std/agent/options"
import {default_llm_caller} from "std/llm/caller"
import {with_retry} from "std/llm/handlers"
import {compose_tool_callers, with_required_reason} from "std/llm/tool_middleware"

const route = agent_model_options({role: "planner"})
const mw = with_required_reason({})
const result = agent_loop(task, system, route.options + {
  loop_until_done: true,
  llm_caller: with_retry(default_llm_caller(), {max_attempts: 3}),
  tool_caller: compose_tool_callers([mw.caller]),
})

*_agent preset wrappers and agent_budget (removed)

The eight one-line wrappers in std/agent/presetsaudit_agent, repair_agent, summary_agent, verify_agent, merge_captain_agent, review_captain_agent, oncall_captain_agent, release_captain_agent — are gone. agent_preset(kind, options?) is the single preset surface; spread its result into agent_loop:

Before (0.9)

import {audit_agent} from "std/agent/presets"

const result = audit_agent("Audit the release", {tools: tools})

After (0.10)

import {agent_preset} from "std/agent/presets"

const opts = agent_preset("audit", {tools: tools})
const result = agent_loop("Audit the release", opts?.system, opts)

agent_budget(kind_or_options, overrides?) is also gone. Preset kinds already install their default iteration_budget; to override, pass an iteration_budget dict (or the "adaptive" string sugar) directly:

const opts = agent_preset("repair", {iteration_budget: {mode: "adaptive", initial: 4, max: 24, extend_by: 2}})

transcript_policy (verified gone)

The pre-0.7 transcript_policy dict on workflow nodes and the workflow_set_transcript_policy builtin were already removed in 0.7 — sessions are the sole surface (see Migrating from 0.6.x to 0.7.0). 0.10 re-verified that no acceptance remains. Note that the worker-resume transcript policy (continue_transcript on worker resume/replay) and the task-plan IR's forward-declared transcript_policy metadata field are different, still-supported mechanisms.