Builtin functions

Complete reference for all built-in functions available in Harn.

Output

FunctionParametersReturnsDescription
log(msg)msg: anynilPrint with [harn] prefix and newline
progress(phase, message, progress?, total?)phase: string, message: string, optional numeric progressnilEmit standalone progress output. Dict options support mode: "spinner" with step, or mode: "bar" with current, total, and optional width
color(text, name)text: any, name: stringstringWrap text with an ANSI foreground color code
bold(text)text: anystringWrap text with ANSI bold styling
dim(text)text: anystringWrap text with ANSI dim styling
set_color_mode(mode)mode: "auto", "always", or "never"nilConfigure process-local ANSI color behavior for color, bold, dim, and std/ansi; auto honors TTY detection, NO_COLOR, and FORCE_COLOR

Type conversion

FunctionParametersReturnsDescription
type_of(value)value: anystringReturns type name: "int", "float", "string", "bool", "nil", "list", "dict", "closure", "taskHandle", "duration", "enum", "struct"
to_string(value)value: anystringConvert to string representation
to_int(value)value: anyint or nilParse/convert to integer. Floats truncate, bools become 0/1; non-finite or out-of-range floats return nil
to_float(value)value: anyfloat or nilParse/convert to float
unreachable(value?)value: any (optional)neverThrows "unreachable code was reached" at runtime. When the argument is a variable, the type checker verifies it has been narrowed to never (exhaustiveness check)
iter(x)x: list, dict, set, string, generator, channel, or iterIter<T>Lift an iterable source into a lazy, single-pass, fused iterator. No-op on an existing iter. Dict iters yield Pair(key, value); string iters yield chars. See Iterator methods
pair(a, b)a: any, b: anyPairConstruct a two-element Pair value. Access via .first / .second, or destructure in a for-loop: for (k, v) in ...

Runtime shape validation

Function parameters with structural type annotations (shapes) are validated at runtime. If a dict or struct argument is missing a required field or has the wrong field type, a descriptive error is thrown before the function body executes.

fn greet(u: {name: string, age: int}) {
  log("${u.name} is ${u.age}")
}

greet({name: "Alice", age: 30})   // OK
greet({name: "Alice"})            // Error: parameter 'u': missing field 'age' (int)

See Error handling -- Runtime shape validation errors for more details.

Result

Harn has a built-in Result type for representing success/failure values without exceptions. Ok and Err create Result.Ok and Result.Err enum variants respectively. When called on a non-Result value, unwrap and unwrap_or pass the value through unchanged.

FunctionParametersReturnsDescription
Ok(value)value: anyResult.OkCreate a Result.Ok value
Err(value)value: anyResult.ErrCreate a Result.Err value
is_ok(result)result: anyboolReturns true if value is Result.Ok
is_err(result)result: anyboolReturns true if value is Result.Err
unwrap(result)result: anyanyExtract Ok value. Throws on Err. Non-Result values pass through
unwrap_or(result, default)result: any, default: anyanyExtract Ok value. Returns default on Err. Non-Result values pass through
unwrap_err(result)result: anyanyExtract Err value. Throws on non-Err

Example:

let good = Ok(42)
let bad = Err("something went wrong")

log(is_ok(good))             // true
log(is_err(bad))             // true

log(unwrap(good))            // 42
log(unwrap_or(bad, 0))       // 0
log(unwrap_err(bad))         // something went wrong

JSON

FunctionParametersReturnsDescription
json_parse(str)str: stringvalueParse JSON string into Harn values. Throws on invalid JSON
json_stringify(value)value: anystringSerialize Harn value to JSON. Closures and handles become null
json_stringify_pretty(value)value: anystringSerialize Harn value to pretty-printed JSON with stable two-space indentation
yaml_parse(str)str: stringvalueParse YAML string into Harn values. Throws on invalid YAML
yaml_stringify(value)value: anystringSerialize Harn value to YAML
toml_parse(str)str: stringvalueParse TOML string into Harn values. Throws on invalid TOML
toml_stringify(value)value: anystringSerialize Harn value to TOML
json_validate(data, schema)data: any, schema: dictboolValidate data against a schema. Returns true if valid, throws with details if not
schema_check(data, schema)data: any, schema: dictResultValidate data against an extended schema and return Result.Ok(data) or Result.Err({message, errors, value?})
schema_parse(data, schema)data: any, schema: dictResultValidate data and return Result.Ok(data) with default values applied recursively, or Result.Err({message, errors, value?})
schema_is(data, schema)data: any, schema: dictboolValidate data against a schema and return true/false without throwing
schema_expect(data, schema, apply_defaults?)data: any, schema: dict, bool (optional)anyValidate data and return the normalized value, throwing on failure
schema_from_json_schema(schema)schema: dictdictConvert a JSON Schema object into Harn's canonical schema dict
schema_from_openapi_schema(schema)schema: dictdictConvert an OpenAPI Schema Object into Harn's canonical schema dict
schema_to_json_schema(schema)schema: dictdictConvert an extended Harn schema into JSON Schema
schema_to_openapi_schema(schema)schema: dictdictConvert an extended Harn schema into an OpenAPI-friendly schema object
schema_extend(base, overrides)base: dict, overrides: dictdictShallow-merge two schema dicts
schema_partial(schema)schema: dictdictRemove required recursively so properties become optional
schema_pick(schema, keys)schema: dict, keys: listdictKeep only selected top-level properties
schema_omit(schema, keys)schema: dict, keys: listdictRemove selected top-level properties
json_extract(text, key?)text: string, key: string (optional)valueExtract JSON from text (strips markdown code fences). If key given, returns that key's value
json_pointer(value, ptr)value: any, ptr: stringvalueRead an RFC 6901 JSON Pointer path. Returns nil when missing
json_pointer_set(value, ptr, new)value: any, ptr: string, new: anyvalueReturn a copy with a JSON Pointer path replaced or inserted at an existing parent
json_pointer_delete(value, ptr)value: any, ptr: stringvalueReturn a copy with a JSON Pointer path removed. Missing paths are unchanged
jq(value, expr)value: any, expr: stringlistEvaluate a jq-like expression and return the emitted stream as a list
jq_first(value, expr)value: any, expr: stringvalueReturn the first jq result, or nil when the expression emits nothing

Type mapping:

JSONHarn
stringstring
integerint
decimal/exponentfloat
true/falsebool
nullnil
arraylist
objectdict

Canonical schema format

The canonical schema is a plain Harn dict. The validator also accepts compatible JSON Schema / OpenAPI Schema Object spellings such as object, array, integer, number, boolean, oneOf, allOf, minLength, maxLength, minItems, maxItems, and additionalProperties, normalizing them into the same internal form.

Schema traversal is bounded before validation runs: canonicalization, JSON Schema/OpenAPI export, runtime validation, and JSON-stream schema setup reject schemas deeper than 128 nested schema nodes, more than 256 $ref expansions, or cyclic local $ref graphs. These failures surface as regular Harn schema errors such as schema depth exceeded (128) or cyclic schema reference: # -> #.

Supported canonical keys:

KeyTypeDescription
typestringExpected type: "string", "int", "float", "bool", "list", "dict", "any"
requiredlistList of required key names (for dicts)
propertiesdictDict mapping property names to sub-schemas (for dicts)
itemsdictSchema to validate each item against (for lists)
additional_propertiesbool or dictWhether unknown dict keys are allowed, or which schema they must satisfy

Example:

let schema = {
  type: "dict",
  required: ["name", "age"],
  properties: {
    name: {type: "string"},
    age: {type: "int"},
    tags: {type: "list", items: {type: "string"}}
  }
}
json_validate(data, schema)  // throws if invalid

Extended schema constraints

The schema builtins support these additional keys:

KeyTypeDescription
nullableboolAllow nil
min / maxint or floatNumeric bounds
min_length / max_lengthintString length bounds
patternstringRegex pattern for strings
enumlistAllowed literal values
constanyExact required literal value
min_items / max_itemsintList length bounds
unionlist of schemasValue must match one schema
all_oflist of schemasValue must satisfy every schema
defaultanyDefault value applied by schema_parse

Example:

let user_schema = {
  type: "dict",
  required: ["name", "age"],
  properties: {
    name: {type: "string", min_length: 1},
    age: {type: "int", min: 0},
    role: {type: "string", enum: ["admin", "user"], default: "user"}
  }
}

let parsed = schema_parse({name: "Ada", age: 36}, user_schema)
log(is_ok(parsed))
log(unwrap(parsed).role)
log(schema_to_json_schema(user_schema).type)

schema_is(...) is useful for dynamic checks and can participate in static type refinement when the schema is a literal (or a variable bound from a literal schema).

The lazy std/schema module provides ergonomic builders such as schema_string(), schema_object(...), schema_union(...), get_typed_result(...), get_typed_value(...), and is_type(...).

Composition helpers:

let public_user = schema_pick(user_schema, ["name", "role"])
let patch_schema = schema_partial(user_schema)
let admin_user = schema_extend(user_schema, {
  properties: {
    name: {type: "string", min_length: 1},
    age: {type: "int", min: 0},
    role: {type: "string", enum: ["admin"], default: "admin"}
  }
})

json_extract

Extracts JSON from LLM responses that may contain markdown code fences or surrounding prose. Handles ```json ... ```, ``` ... ```, and bare JSON with surrounding text. Uses balanced bracket matching to correctly extract nested objects and arrays from mixed prose.

let result = llm_call("Return JSON with name and age")
let data = json_extract(result.text)         // parse, stripping fences
let name = json_extract(result.text, "name") // extract just one key

JSON pointer and jq-like queries

json_pointer implements RFC 6901 addressing, including ~0 for ~ and ~1 for /. json_pointer_set and json_pointer_delete return mutated copies instead of changing the input value in place. Setting a dict key inserts or replaces it when the parent exists; setting a list index replaces that element, and - appends.

jq supports the v1 scripting subset: identity, field access, quoted-key access, array iteration/index/slice, pipes, commas, length, keys, values, type, map(...), select(...), ==, !=, <, >, and, or, not, object construction, and recursive descent. It always returns the emitted stream as a list; jq_first is the convenience form for single-result queries.

let api = json_parse(response.body)
let email = json_pointer(api, "/users/0/email")
let active_emails = jq(api, ".users[] | select(.active == true) | .email")
let summary = jq_first(api, "{ count: .users | length, next: .meta.next }")

Multipart forms

FunctionParametersReturnsDescription
multipart_parse(body, content_type, options?)body: bytes or string, content_type: string, options: dictdictParse a buffered multipart/form-data request body. Options support max_total_bytes, max_field_bytes, and max_fields
multipart_field_bytes(field)field: dictbytesReturn a parsed field's raw bytes
multipart_field_text(field)field: dictstringDecode a parsed field's bytes as UTF-8, throwing on invalid text
multipart_form_data(fields, options?)fields: list, options: dictdictDeterministically build {content_type, boundary, body} test fixtures from field dicts

multipart_parse returns {boundary, fields, field_count, total_bytes}. Each field is {name, filename, content_type, headers, bytes, text}. text is nil when the uploaded bytes are not valid UTF-8; use multipart_field_text(field) when invalid UTF-8 should be an error.

let fixture = multipart_form_data([
  {name: "title", content: "Quarterly report"},
  {
    name: "upload",
    filename: "report.bin",
    content_type: "application/octet-stream",
    content: bytes_from_hex("000102ff"),
  },
])

let form = multipart_parse(fixture.body, fixture.content_type, {
  max_total_bytes: 1048576,
  max_field_bytes: 262144,
  max_fields: 8,
})

let title = multipart_field_text(form.fields[0])
let uploaded = multipart_field_bytes(form.fields[1])
log(title)
log(bytes_to_hex(uploaded))

Math

FunctionParametersReturnsDescription
abs(n)n: int or floatint or floatAbsolute value
ceil(n)n: floatintCeiling (rounds up). Ints pass through unchanged
floor(n)n: floatintFloor (rounds down). Ints pass through unchanged
round(n)n: floatintRound to nearest integer. Ints pass through unchanged
sqrt(n)n: int or floatfloatSquare root
pow(base, exp)base: number, exp: numberint or floatExponentiation. Returns int when both args are int and exp is non-negative
min(a, b)a: number, b: numberint or floatMinimum of two values. Returns float if either argument is float
max(a, b)a: number, b: numberint or floatMaximum of two values. Returns float if either argument is float
rng_seed(seed)seed: intrngCreate a reproducible RNG handle
random()nonefloatRandom float in [0, 1)
random(rng)rng: rngfloatRandom float from a seeded RNG handle
random_int(min, max)min: int, max: intintRandom integer in [min, max] inclusive
random_int(rng, min, max)rng: rng, min: int, max: intintRandom integer from a seeded RNG handle
random_choice(list)list: listany or nilRandom element from a list, or nil for an empty list
random_choice(rng, list)rng: rng, list: listany or nilRandom element using a seeded RNG handle
random_shuffle(list)list: listlistShuffled copy of a list
random_shuffle(rng, list)rng: rng, list: listlistShuffled copy using a seeded RNG handle
mean(items)items: list[number]floatArithmetic mean of a numeric list
median(items)items: list[number]floatMedian of a numeric list
variance(items, sample?)items: list[number], sample: boolfloatPopulation variance, or sample variance when sample = true
stddev(items, sample?)items: list[number], sample: boolfloatPopulation standard deviation, or sample mode when sample = true
percentile(items, p)items: list[number], p: 0..100floatR-7 percentile interpolation

Trigonometry

FunctionParametersReturnsDescription
sin(n)n: floatfloatSine (radians)
cos(n)n: floatfloatCosine (radians)
tan(n)n: floatfloatTangent (radians)
asin(n)n: floatfloatInverse sine
acos(n)n: floatfloatInverse cosine
atan(n)n: floatfloatInverse tangent
atan2(y, x)y: float, x: floatfloatTwo-argument inverse tangent

Logarithms and exponentials

FunctionParametersReturnsDescription
log2(n)n: floatfloatBase-2 logarithm
log10(n)n: floatfloatBase-10 logarithm
ln(n)n: floatfloatNatural logarithm
exp(n)n: floatfloatEuler's number raised to the power n

Constants and utilities

FunctionParametersReturnsDescription
pifloatThe constant pi (3.14159...)
efloatEuler's number (2.71828...)
sign(n)n: int or floatintSign of a number: -1, 0, or 1
is_nan(n)n: floatboolCheck if value is NaN
is_infinite(n)n: floatboolCheck if value is infinite

Sets

FunctionParametersReturnsDescription
set(items?)items: list (optional)setCreate a new set, optionally from a list
set_add(s, value)s: set, value: anysetAdd a value to a set, returns new set
set_remove(s, value)s: set, value: anysetRemove a value from a set, returns new set
set_contains(s, value)s: set, value: anyboolCheck if set contains a value
set_union(a, b)a: set, b: setsetUnion of two sets
set_intersect(a, b)a: set, b: setsetIntersection of two sets
set_difference(a, b)a: set, b: setsetDifference (elements in a but not b)
set_symmetric_difference(a, b)a: set, b: setsetElements in either but not both
set_is_subset(a, b)a: set, b: setboolTrue if all elements of a are in b
set_is_superset(a, b)a: set, b: setboolTrue if a contains all elements of b
set_is_disjoint(a, b)a: set, b: setboolTrue if a and b share no elements
to_list(s)s: setlistConvert a set to a list

Set methods (dot syntax)

Sets also support method syntax: my_set.union(other).

MethodParametersReturnsDescription
.count() / .len()noneintNumber of elements
.empty()noneboolTrue if set is empty
.contains(val)val: anyboolCheck membership
.add(val)val: anysetNew set with val added
.remove(val)val: anysetNew set with val removed
.union(other)other: setsetUnion
.intersect(other)other: setsetIntersection
.difference(other)other: setsetElements in self but not other
.symmetric_difference(other)other: setsetElements in either but not both
.is_subset(other)other: setboolTrue if self is a subset of other
.is_superset(other)other: setboolTrue if self is a superset of other
.is_disjoint(other)other: setboolTrue if no shared elements
.to_list()nonelistConvert to list
.map(fn)fn: closuresetTransform elements (deduplicates)
.filter(fn)fn: closuresetKeep elements matching predicate
.any(fn)fn: closureboolTrue if any element matches
.all(fn) / .every(fn)fn: closureboolTrue if all elements match

String functions

FunctionParametersReturnsDescription
len(value)value: string, list, or dictintLength of string (chars), list (items), or dict (keys)
trim(str)str: stringstringRemove leading and trailing whitespace
lowercase(str)str: stringstringConvert to lowercase
uppercase(str)str: stringstringConvert to uppercase
split(str, sep)str: string, sep: stringlistSplit string by separator
starts_with(str, prefix)str: string, prefix: stringboolCheck if string starts with prefix
ends_with(str, suffix)str: string, suffix: stringboolCheck if string ends with suffix
contains(str, substr)str: string, substr: stringboolCheck if string contains substring. Also works on lists
replace(str, old, new)str: string, old: string, new: stringstringReplace all occurrences
join(list, sep)list: list, sep: stringstringJoin list elements with separator
substring(str, start, end?)str: string, start: int, end: intstringExtract the character range [start, end); end defaults to the string length. Matches .substring, s[a:b], and list.slice
chars(str)str: stringlistMaterialize a string into a list of single-character strings in one linear pass (ASCII chars are interned). Use this for cursor-style source scanning — see Scanning large text — instead of repeated substring/s[i], which are O(n) per call
unicode_normalize(str, form)str: string, form: "NFC"|"NFD"|"NFKC"|"NFKD"stringNormalize Unicode into the requested form
unicode_graphemes(str)str: stringlistSplit a string into extended grapheme clusters
str_pad(str, width, char?, side?)str: string, width: int, char: string, side: "left"|"right"|"both"stringPad to a grapheme width using the given fill character
format(template, ...)template: string, args: anystringFormat string with {} placeholders. With a dict as the second arg, supports named {key} placeholders
repeat(str, n)str: string, n: intstringConcatenate str with itself n times. Rejects pathological sizes
indent(text, prefix?)text: string, prefix: string (default " ")stringPrefix every non-blank line with the given indent
dedent(text)text: stringstringStrip the longest common leading whitespace from every non-blank line
word_wrap(text, width?)text: string, width: int (default 80)stringGreedy word-wrap that preserves existing newlines and never splits words mid-token

String methods (dot syntax)

These are called on string values with dot notation: "hello".uppercase().

MethodParametersReturnsDescription
.trim()nonestringRemove leading/trailing whitespace
.trim_start()nonestringRemove leading whitespace only
.trim_end()nonestringRemove trailing whitespace only
.lines()nonelistSplit string by newlines
.char_at(index)index: intstring or nilCharacter at index (nil if out of bounds)
.index_of(substr)substr: stringintFirst character offset of substring (-1 if not found)
.last_index_of(substr)substr: stringintLast character offset of substring (-1 if not found)
.lower() / .to_lower()nonestringLowercase string
.len()noneintCharacter count
.upper() / .to_upper()nonestringUppercase string
.chars()nonelistList of single-character strings
.reverse()nonestringReversed string
.repeat(n)n: intstringRepeat n times
.pad_left(width, char?)width: int, char: stringstringPad to width with char (default space)
.pad_right(width, char?)width: int, char: stringstringPad to width with char (default space)

Scanning large text

Strings are stored as UTF-8, so random character access — s[i], s[a:b], s.count, and substring(s, a, b) — is O(n) in the string length. A per-character cursor loop built from those calls is therefore O(n²) and stalls on multi-kilobyte source files.

To scan source text, materialize the string once into a list of single-character strings with chars(str) (or str.chars()), then index the list — list access is O(1), and chars interns ASCII characters so the materialization does not allocate per character:

let cs = chars(src)
var i = 0
var braces = 0
while i < cs.count {
  if cs[i] == "{" { braces = braces + 1 }
  i = i + 1
}

Prefer str.lines(), split(str, sep), or the regex_* builtins when a line-, token-, or pattern-oriented scan suffices.

List methods (dot syntax)

MethodParametersReturnsDescription
.map(fn)fn: closurelistTransform each element
.filter(fn)fn: closurelistKeep elements where fn returns truthy
.reduce(init, fn)init: any, fn: closureanyFold with accumulator
.find(fn)fn: closureany or nilFirst element matching predicate
.find_index(fn)fn: closureintIndex of first match (-1 if not found)
.any(fn)fn: closureboolTrue if any element matches
.all(fn) / .every(fn)fn: closureboolTrue if all elements match
.none(fn?)fn: closureboolTrue if no elements match (no arg: checks emptiness)
.first(n?)n: int (optional)any or listFirst element, or first n elements
.last(n?)n: int (optional)any or listLast element, or last n elements
.partition(fn)fn: closurelistSplit into [[truthy], [falsy]]
.group_by(fn)fn: closuredictGroup into dict keyed by fn result
.sort() / .sort_by(fn)fn: closure (optional)listSort (natural or by key function)
.min() / .max()noneanyMinimum/maximum value
.min_by(fn) / .max_by(fn)fn: closureanyMin/max by key function
.chunk(size)size: intlistSplit into chunks of size
.window(size)size: intlistSliding windows of size
.each_cons(size)size: intlistSliding windows of size
.compact()nonelistRemove nil values
.unique()nonelistRemove duplicates
.flatten()nonelistFlatten one level of nesting
.flat_map(fn)fn: closurelistMap then flatten
.tally()nonedictFrequency count: {value: count}
.zip(other)other: listlistPair elements from two lists
.enumerate()nonelistList of {index, value} dicts
.take(n) / .skip(n)n: intlistFirst/remaining n elements
.sum()noneint or floatSum of numeric values
.join(sep?)sep: stringstringJoin to string
.reverse()nonelistReversed list
.push(item) / .pop()item: anylistNew list with item added/removed (immutable)
.contains(item)item: anyboolCheck if list contains item
.index_of(item)item: anyintIndex of item (-1 if not found)
.slice(start, end?)start: int, end: intlistSlice with negative index support

Collection helper builtins

FunctionParametersReturnsDescription
chunk(list, size)list: list, size: intlistSplit into chunks of size
window(list, size, step?)list: list, size: int, step: intlistSliding windows with optional stride
group_by(list, fn)list: list, fn: closuredictGroup into a dict keyed by callback result
partition(list, fn)list: list, fn: closuredictSplit into {match, no_match} lists
dedup_by(list, fn)list: list, fn: closurelistKeep the first item for each callback-derived key
flat_map(list, fn)list: list, fn: closurelistMap then flatten one level
clone(value)value: anyanyShallow copy. Dicts and lists become fresh allocations independent of the source; primitives return by value
deep_clone(value)value: anyanyRecursive deep copy. Nested dicts/lists are duplicated top-to-bottom
deep_merge(a, b)a: dict, b: dictdictRecursive merge — when both sides have a dict at the same key the dicts merge; otherwise right wins
unique(list)list: listlistRemove duplicates, preserving first-seen order. Structural equality
dict_from_pairs(pairs)pairs: list of [key, value]dictBuild a dict from a list of pairs (later pairs override earlier ones)
index_by(items, fn)items: list, fn: closuredictBuild a lookup table keyed by fn(item)
to_xml(value, options?)value: any, options: dict (root, item_tag, pretty, declaration)stringConvert a Harn value into XML. Dicts → tag trees; lists → repeated <item> children
from_xml(text, options?)text: string, options: dict (preserve_repeated_tag)dictParse XML back into a dict tree. Repeated children collapse into a list by default; preserve_repeated_tag: true keeps the inner tag
jsonrpc_call(url, method, params?, options?)url: string, method: string, params: any, options: dict (headers, id, notify, timeout_ms)anyPOST a JSON-RPC 2.0 envelope. Returns result on success, raises a structured error dict on failure
jsonrpc_batch(url, calls, options?)url: string, calls: list of {method, params?, id?, notify?} dicts, options: dict (headers, timeout_ms)listPOST a JSON-RPC 2.0 batch. Returns a list aligned with input order; per-call errors arrive as {jsonrpc_error: true, ...} dicts inside the list

Iterator methods

Eager list/dict/set/string methods listed above are unchanged — they still return eager collections. Lazy iteration is opt-in via .iter(), which lifts a list, dict, set, string, generator, or channel into an Iter<T> value. Iterators are single-pass, fused, and snapshot — they Rc-clone the backing collection, so mutating the source after .iter() does not affect the iter.

On a dict, .iter() yields Pair(key, value) values (use .first / .second, or destructure in a for-loop). String iteration yields chars (Unicode scalar values).

Printing with log(it) renders <iter> or <iter (exhausted)> and does not drain the iterator.

Lazy combinators (return a new Iter)

MethodParametersReturnsDescription
.iter()noneIter<T>Lift a source into an iter; no-op on an existing iter
.map(fn)fn: closureIter<U>Lazily transform each item
.filter(fn)fn: closureIter<T>Lazily keep items where fn returns truthy
.flat_map(fn)fn: closureIter<U>Map then flatten, lazily
.take(n)n: intIter<T>First n items
.skip(n)n: intIter<T>Drop first n items
.take_while(fn)fn: closureIter<T>Items until predicate first returns falsy
.skip_while(fn)fn: closureIter<T>Drop items while predicate is truthy
.zip(other)other: iterIter<Pair<T, U>>Pair items from two iters, stops at shorter
.enumerate()noneIter<Pair<int, T>>Pair each item with a 0-based index
.chain(other)other: iterIter<T>Yield items from self, then from other
.chunks(n)n: intIter<list<T>>Non-overlapping fixed-size chunks
.windows(n)n: intIter<list<T>>Sliding windows of size n

Sinks (drain the iter, return an eager value)

MethodParametersReturnsDescription
.to_list()nonelistCollect all items into a list
.to_set()nonesetCollect all items into a set
.to_dict()nonedictCollect Pair(key, value) items into a dict
.count()noneintCount remaining items
.sum()noneint or floatSum of numeric items
.min() / .max()noneanyMin/max item
.reduce(init, fn)init: any, fn: closureanyFold with accumulator
.first() / .last()noneany or nilFirst/last item
.any(fn)fn: closureboolTrue if any remaining item matches
.all(fn)fn: closureboolTrue if all remaining items match
.find(fn)fn: closureany or nilFirst item matching predicate
.for_each(fn)fn: closurenilInvoke fn on each remaining item

Path functions

FunctionParametersReturnsDescription
dirname(path)path: stringstringDirectory component of path
basename(path)path: stringstringFile name component of path
extname(path)path: stringstringFile extension including dot (e.g., .harn)
path_join(parts...)parts: stringsstringJoin path components
path_workspace_info(path, workspace_root?)path: string, workspace_root?: stringdictClassify a path as workspace_relative, host_absolute, or invalid, and project both workspace-relative and host-absolute forms when known
path_workspace_normalize(path, workspace_root?)path: string, workspace_root?: stringstring or nilNormalize a path into workspace-relative form when it is safely inside the workspace (including common leading-slash drift like /packages/...)

File I/O

Capability-aware scripts should call the harness.fs.* methods. The free filesystem builtins remain supported as thin aliases for existing scripts.

FunctionParametersReturnsDescription
read_file(path)path: stringstringRead entire file as UTF-8 string, or return raw source for embedded std/...harn.prompt assets. Throws on failure. Deprecated in favor of read_file_result for new code; the throwing form remains supported.
read_file_result(path)path: stringResult<string, string>Non-throwing read: returns Result.Ok(content) on success or Result.Err(message) on failure. Shares read_file's content cache for filesystem files and also supports embedded std/...harn.prompt assets
write_file(path, content)path: string, content: stringnilWrite string to file. Throws on failure
append_file(path, content)path: string, content: stringnilAppend string to file, creating it if it doesn't exist. Throws on failure
copy_file(src, dst)src: string, dst: stringnilCopy a file. Throws on failure
delete_file(path)path: stringnilDelete a file or directory (recursive). Throws on failure
file_exists(path)path: stringboolCheck if a file or directory exists
list_dir(path?)path: string (default ".")listList directory contents as sorted list of file names. Throws on failure
walk_dir(path, options?)path: string, options: dictlist or handle dictRecursively list files/directories. Options: max_depth, follow_symlinks, long_running/background
glob(pattern, base_or_options?, options?) / harness.fs.glob(pattern, base_or_options?, options?)pattern: string, base: string or options: dictlist or handle dictMatch files under a base directory. Set long_running/background in options to return a handle
find_text(root, pattern, options?) / harness.fs.find_text(root, pattern, options?)root: string, pattern: string, options: dictlist, bool, int, or handle dictSearch files under a root and return {path, line, col, column, text} hits. Options: mode (hits, exists, count), preset (default, source, all), include, exclude/ignore, max_depth, max_filesize/max_file_size, threads, parallel, follow_symlinks, include_hidden, respect_gitignore, case_sensitive/case_insensitive, fixed_strings, max_matches, long_running/background
mkdir(path)path: stringnilCreate directory and all parent directories. Throws on failure
stat(path)path: stringdictFile metadata: {size, is_file, is_dir, readonly, modified}. Throws on failure
temp_dir()nonestringSystem temporary directory path
mkdtemp(prefix?) / harness.fs.mkdtemp(prefix?)prefix: string (default "harn-")stringCreate a new uniquely-named directory under the host temp dir and return its absolute path. The caller owns the directory's lifecycle — it is not cleaned up automatically; pair with harness.fs.delete(path) when finished. Path separators in prefix are stripped so callers cannot smuggle subdirectory trees
render(path, bindings?)path: string, bindings: dictstringRead a template asset and render it. path may be source-relative, @/<rel> (anchored at the calling file's project root), @<alias>/<rel> (anchored at a [asset_roots] entry in harn.toml), or an embedded std/...harn.prompt stdlib prompt asset — see Package-root prompt assets. The template language supports {{ name }} interpolation (with nested paths and filters), {{ if }} / {{ elif }} / {{ else }} / {{ end }}, {{ for item in xs }} ... {{ end }} (with {{ loop.index }} etc.), {{ include "..." }} partials, logical {{ section "task" }} ... {{ endsection }} prompt sections, {{# comments #}}, {{ raw }} ... {{ endraw }} verbatim blocks, and {{- -}} whitespace trim markers. See the Prompt templating reference for the full grammar and filter list. Source-relative paths called from an imported module resolve relative to that module's directory, not the entry pipeline. Without bindings, just reads the file
render_prompt(path, bindings?)path: string, bindings: dictstringPrompt-oriented alias of render(...). Use this for .harn.prompt / .prompt assets when you want the asset to be surfaced explicitly in bundle manifests and preflight output. Accepts the same @/<rel>, @<alias>/<rel>, and embedded std/...harn.prompt forms as render(...)
render_string(template, bindings?)template: string, bindings: dictstringRender an inline template string with the same template engine as render(...). Useful when a library wants to embed a template directly in source instead of shipping a separate .prompt file. {{ include "..." }} resolves relative to the current module's asset root, with @/<rel> and @<alias>/<rel> supported

Environment and system

FunctionParametersReturnsDescription
env(name)name: stringstring or nilRead environment variable
env_or(name, default)name: string, default: anystring or defaultRead environment variable, or return default when unset. One-line replacement for the common let v = env(K); if v { v } else { default } pattern
timestamp()nonefloatUnix timestamp in seconds with sub-second precision
elapsed()noneintMilliseconds since VM startup
exec(cmd, args...)cmd: string, args: stringsdictExecute external command. Returns stdout/stderr plus status metadata and success
exec_at(dir, cmd, args...)dir: string, cmd: string, args: stringsdictExecute external command inside a specific directory
shell(cmd)cmd: stringdictExecute command via shell. Returns stdout/stderr plus status metadata and success
shell_at(dir, cmd)dir: string, cmd: stringdictExecute shell command inside a specific directory
harness.process.spawn_captured(opts)opts: dictdictRun an external command synchronously through the Harness process capability. opts is {cmd, args?, cwd?, env?, stdin?, timeout_ms?} and supports feeding a stdin payload plus a per-call timeout. Returns {exit_code, stdout, stderr, duration_ms, success, timed_out}. On timeout the child is killed, exit_code = -1, success = false, and timed_out = true
spawn_captured(opts)opts: dictdictLegacy alias for harness.process.spawn_captured(opts) when no Harness handle is available
term_width()noneintCurrent terminal column count. Alias for the canonical harness.term.width() surface; reads COLUMNS env first, falls back to the platform window size, then to 80
term_height()noneintCurrent terminal row count. Alias for the canonical harness.term.height() surface; reads LINES env first, falls back to the platform window size, then to 24
exit(code)code: int (default 0)neverTerminate the process
username()nonestringCurrent OS username
hostname()nonestringMachine hostname
platform()nonestringOS name: "darwin", "linux", or "windows"
arch()nonestringCPU architecture (e.g., "aarch64", "x86_64")
uuid()nonestringGenerate a random v4 UUID
uuid_parse(str)str: stringstring or nilParse and canonicalize a UUID string, or return nil if invalid
uuid_v5(namespace, name)namespace: UUID or "dns"|"url"|"oid"|"x500", name: stringstringGenerate a deterministic namespaced v5 UUID
uuid_v7()nonestringGenerate a time-ordered v7 UUID
uuid_nil()nonestringReturn the all-zero nil UUID
home_dir()nonestringUser's home directory path
pid()noneintCurrent process ID
cwd()nonestringCurrent working directory
execution_root()nonestringDirectory used for source-relative execution helpers such as exec_at(...) / shell_at(...)
asset_root()nonestringDirectory used for source-relative asset helpers such as render(...) / render_prompt(...)
source_dir()nonestringDirectory of the currently-executing .harn file (falls back to cwd)
project_root()nonestring or nilNearest ancestor directory containing harn.toml
runtime_paths()nonedictResolved runtime path model: {execution_root, asset_root, state_root, run_root, worktree_root}
date_iso()nonestringCurrent UTC time in ISO 8601 format (e.g., "2026-03-29T14:30:00.123Z")

For interactive terminal presentation, import std/tui. It provides page({title?, body, format?, no_pager?, footer?}), terminal_width(), rule(), and clear() on top of the TTY-aware stdout primitives.

Regular expressions

FunctionParametersReturnsDescription
regex_match(pattern, text, flags?)pattern: string, text: string, flags: stringlist or nilFind all non-overlapping matches. Optional flags: i, m, s, x
regex_split(text, pattern, flags?)text: string, pattern: string, flags: stringlistSplit text by regex matches
regex_replace(pattern, replacement, text, flags?)pattern: string, replacement: string, text: string, flags: stringstringReplace all matches. Optional flags: i, m, s, x. Throws on invalid regex
regex_captures(pattern, text, flags?)pattern: string, text: string, flags: stringlistFind all matches with capture group details and positions. Optional flags: i, m, s, x

regex_captures

Returns a list of dicts, one per match. Each dict contains:

  • match -- the full matched string
  • groups -- a list of positional capture group values (from (...))
  • start / end -- character (code-point) offsets of the match in text, consistent with substring/index_of/len
  • line -- 1-based line of the match start (the equivalent of text.count("\n", 0, start) + 1), for positional diagnostics
  • Named capture groups (from (?P<name>...)) appear as additional keys
let results = regex_captures("(\\w+)@(\\w+)", "alice@example bob@test")
// [
//   {match: "alice@example", groups: ["alice", "example"], start: 0, end: 13, line: 1},
//   {match: "bob@test", groups: ["bob", "test"], start: 14, end: 22, line: 1}
// ]

Named capture groups are added as top-level keys on each result dict:

let named = regex_captures("(?P<user>\\w+):(?P<role>\\w+)", "alice:admin")
// [{match: "alice:admin", groups: ["alice", "admin"], user: "alice", role: "admin"}]

Returns an empty list if there are no matches. Throws on invalid regex.

Encoding

FunctionParametersReturnsDescription
base64_encode(string)string: stringstringBase64 encode a string (standard alphabet with padding)
base64_decode(string)string: stringstringBase64 decode a string. Throws on invalid input
base64url_encode(string)string: stringstringBase64 encode a string with the URL-safe alphabet and no padding
base64url_decode(string)string: stringstringDecode a URL-safe base64 string without padding. Throws on invalid input
base32_encode(string)string: stringstringBase32 encode a string using the RFC 4648 alphabet with padding
base32_decode(string)string: stringstringDecode a base32 string. Throws on invalid input
hex_encode(string)string: stringstringHex encode a string as lowercase ASCII
hex_decode(string)string: stringstringDecode a hex string. Throws on invalid input
url_encode(string)string: stringstringURL percent-encode a string. Unreserved characters (alphanumeric, -, _, ., ~) pass through unchanged
url_decode(string)string: stringstringDecode a URL-encoded string. Decodes %XX sequences and + as space

Example:

let encoded = base64_encode("Hello, World!")
log(encoded)                  // SGVsbG8sIFdvcmxkIQ==
log(base64_decode(encoded))   // Hello, World!
log(base64url_encode(">>>???///"))     // Pj4-Pz8_Ly8v
log(base32_encode("foobar"))           // MZXW6YTBOI======
log(hex_encode("hello"))               // 68656c6c6f
log(hex_decode("68656c6c6f"))          // hello
log(url_encode("hello world"))         // hello%20world
log(url_decode("hello%20world"))       // hello world
log(url_encode("a=1&b=2"))             // a%3D1%26b%3D2
log(url_decode("hello+world"))         // hello world

Hashing

FunctionParametersReturnsDescription
sha256(string)string: stringstringSHA-256 hash, returned as a lowercase hex-encoded string
harness.crypto.sha256(string_or_bytes)string_or_bytes: string or bytesstringHarness-scoped SHA-256 hash, returned as lowercase hex. Accepts bytes directly without stringifying, so content-addressing scripts can hash binary payloads unambiguously
sha256_hex(string_or_bytes)string_or_bytes: string or bytesstringCompatibility alias for harness.crypto.sha256(...). For string inputs the digest matches sha256(...)
md5(string)string: stringstringMD5 hash, returned as a lowercase hex-encoded string

Example:

fn main(harness: Harness) {
  log(sha256("hello"))  // 2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
  log(harness.crypto.sha256(bytes_from_string("hello")))
  log(md5("hello"))     // 5d41402abc4b2a76b9719d911017c592
}

HMAC and signature comparison

FunctionParametersReturnsDescription
hmac_sha256(key, message)key: string, message: stringstringHMAC-SHA256 as a lowercase hex-encoded string. Most webhook providers (GitHub, Stripe) send signatures in this form
hmac_sha256_base64(key, message)key: string, message: stringstringHMAC-SHA256 as standard base64 (used by Slack-style signatures)
aws_sigv4_headers(spec)spec: dictdictDeterministically sign one AWS HTTP request with explicit credentials. Returns signed headers plus canonical request metadata; does not perform network I/O
constant_time_eq(a, b)a: string, b: stringboolTiming-safe string equality. Always use this to compare HMAC signatures — plain == can leak the signature byte-by-byte through timing differences
signed_url(base, claims, secret, expires_at, options?)base: string, claims: dict, secret: string, expires_at: int, options: dictstringCreate a short-lived HMAC-SHA256 signed absolute URL or absolute path. The signature is URL-safe base64 without padding
verify_signed_url(url, secret_or_keys, now, options?)url: string, secret_or_keys: string or dict, now: int, options: dictdictVerify a signed URL/path with constant-time signature comparison and optional clock skew. Returns {valid, reason, signature_valid, expired, expires_at, kid, claims}
jwt_sign(alg, claims, private_key)alg: string, claims: dict, private_key: stringstringSign a compact JWT/JWS using a PEM private key. Supports ES256 with P-256 EC private keys and RS256 with RSA private keys

Example (GitHub-style webhook signature verification):

let signature = "sha256=" + hmac_sha256(secret, raw_body)
if !constant_time_eq(signature, request_signature) {
  throw "invalid signature"
}

Example (one AWS REST/JSON request against http_mock):

let body = "{\"TableName\":\"Items\"}"
let url = "https://dynamodb.us-east-1.amazonaws.com/"
http_mock("POST", url, {status: 200, body: "{\"ok\":true}", headers: {}})

let signed = aws_sigv4_headers({
  method: "POST",
  url: url,
  service: "dynamodb",
  region: "us-east-1",
  body: body,
  access_key_id: access_key_id,
  secret_access_key: secret_access_key,
  session_token: session_token,
  headers: {
    "Content-Type": "application/x-amz-json-1.0",
    "X-Amz-Target": "DynamoDB_20120810.DescribeTable",
  },
  timestamp: date_format(date_now().timestamp, "%Y%m%dT%H%M%SZ"),
})

let response = harness.net.request("POST", url, {
  body: body,
  headers: signed.headers,
})

aws_sigv4_headers(...) is intentionally not an AWS SDK. It exists so focused connector packages can sign a single AWS call when a product workflow needs one. Credentials must come from the caller or secret provider. timestamp is required to keep signing deterministic; pass a fixed value in tests or format the current time at the call site. Temporary credentials are supported via session_token, which emits and signs X-Amz-Security-Token. Errors name only invalid fields or signing rules and do not include access keys, secret access keys, session tokens, derived signing keys, canonical request bodies, or signed headers. Normal transcript and http_mock_calls() redaction treat Authorization and X-Amz-Security-Token as sensitive headers.

Example (short-lived receipt or artifact link):

let expires_at = timestamp() + 300
let link = signed_url(
  "https://portal.example.test/receipts/r_123",
  {artifact: "transcript.json"},
  receipt_secret,
  expires_at,
  {kid: "v2"},
)

let verified = verify_signed_url(
  link,
  {v1: old_receipt_secret, v2: receipt_secret},
  timestamp(),
  {skew_seconds: 30},
)
if !verified.valid {
  throw "invalid receipt link: " + verified.reason
}

signed_url accepts either an absolute URL with a host or an absolute path beginning with /. Existing query parameters and claims are merged, reserved parameters are then added, and the query is canonicalized by percent-encoding each key/value with RFC 3986 unreserved characters left plain and sorting encoded pairs lexicographically. Paths preserve /, preserve existing %XX escapes with uppercase hex, and percent-encode other non-unreserved bytes. The signed payload is the version marker, canonical resource (origin + path for URLs, path for paths), and canonical query without the signature. Default parameter names are exp, kid, and sig; override them with expires_param, kid_param, and signature_param in options. kid is optional when signing; verification can use either one secret string or a dict mapping key ids to secrets.

JWT signing expects claims to be a JSON object. The private key must be PEM encoded: ES256 accepts PKCS#8 EC private keys such as -----BEGIN PRIVATE KEY-----; RS256 accepts RSA private keys such as -----BEGIN RSA PRIVATE KEY----- or PKCS#8 private keys. Invalid algorithms, non-dict claims, and malformed PEM keys throw runtime errors.

let token = jwt_sign(
  "ES256",
  {iss: app_id, iat: timestamp(), exp: timestamp() + 600},
  read_file("github-app-private-key.pem"),
)

Cookies and sessions

FunctionParametersReturnsDescription
cookie_parse(headers)headers: string, list, or dictdictParse request Cookie header values into {cookies, pairs, duplicates, invalid}. cookies keeps the first value for each name; duplicates records all values for repeated names
cookie_serialize(name, value, options?)name: string, value: string, options: dictstringSerialize one Set-Cookie value. Options support http_only, secure, same_site, path, domain, max_age, and expires
cookie_delete(name, options?)name: string, options: dictstringSerialize a deletion cookie with Max-Age=0 and a Unix epoch Expires; secure session defaults are applied unless overridden
cookie_sign(value, secret)value: string, secret: stringstringReturn value.signature using HMAC-SHA256 and URL-safe base64 for tamper-evident cookie values
cookie_verify(signed_value, secret)signed_value: string, secret: stringdictVerify a signed cookie value and return {ok, value, error} without throwing on signature failure
session_sign(payload, secret)payload: any JSON value, secret: stringstringReturn a stateless signed session token containing the JSON payload
session_verify(token, secret)token: string, secret: stringdictVerify a stateless session token and return {ok, payload, error} without throwing on signature failure
session_cookie(name, payload, secret, options?)name: string, payload: any JSON value, secret: string, options: dictstringSerialize a signed session cookie. Defaults are Path=/, HttpOnly, Secure, and SameSite=Lax
session_from_cookies(headers, name, secret)headers: string/list/dict, name: string, secret: stringdictParse request cookies, read name, and verify it as a stateless session token
cookie_round_trip(request_cookie?, set_cookie)request_cookie: string/list/dict, set_cookie: string/list/dictdictTest helper that applies response Set-Cookie headers to an existing request cookie header and returns {cookie_header, cookies} for the next request

cookie_parse accepts a raw Cookie string, a list of strings, or a headers dict containing Cookie/cookie. Empty segments are ignored. Invalid segments are skipped and reported in invalid. When the same cookie name appears more than once, cookies[name] keeps the first value and duplicates[name] contains all observed values in wire order.

let parsed = cookie_parse("sid=abc; theme=light; sid=old")
log(parsed.cookies.sid)       // abc
log(parsed.duplicates.sid[1]) // old

cookie_serialize validates names and values before writing a Set-Cookie header. SameSite=None requires Secure so insecure cross-site cookies are rejected early.

let header = cookie_serialize("theme", "dark", {
  path: "/",
  max_age: 3600,
  http_only: true,
  secure: true,
  same_site: "Strict",
})

session_* helpers are stateless: all trusted session data lives inside the signed cookie token. For store-backed sessions, put only an opaque session ID in the cookie and store the mutable server-side state with store_*, shared_map_*, or an application database.

let set_cookie = session_cookie("harn_session", {user: "alice"}, secret)
let next_request = cookie_round_trip(set_cookie)
let session = session_from_cookies(next_request.cookie_header, "harn_session", secret)
if !session.ok {
  throw "invalid session"
}

Date/Time

For civil-time workflows, import std/calendar. It provides ISO week and quarter helpers, start/end-of-period boundaries, DST-explicit local datetime construction, supported country/timezone metadata, and business-calendar helpers such as add_business_days and next_business_day.

FunctionParametersReturnsDescription
date_now()nonedictCurrent UTC datetime as dict with year, month, day, hour, minute, second, weekday, timestamp, and iso8601 fields
date_now_iso()nonestringCurrent UTC datetime as RFC 3339 / ISO 8601 string
date_parse(str)str: stringint or floatParse RFC 3339 / ISO 8601 strings (including offsets and fractional seconds) into a Unix timestamp. Falls back to legacy numeric-component extraction for malformed legacy inputs, but validates the resulting calendar date
date_format(dt, format?, tz?)dt: float, int, or dict; format: string (default "%Y-%m-%d %H:%M:%S"); tz: IANA timezone stringstringFormat a timestamp or date dict using chrono/strftime format codes such as %Y, %m, %d, %H, %M, %S, %A, %B, %Z, %z, %:z, %f, %3f, and %s. Negative pre-epoch timestamps are supported
date_in_zone(dt, tz)dt: float, int, or dict; tz: IANA timezone stringdictConvert a timestamp into timezone-local fields: year, month, day, hour, minute, second, weekday, zone, offset_seconds, timestamp, and iso8601
date_to_zone(dt, tz)dt: float, int, or dict; tz: IANA timezone stringstringConvert a timestamp to an RFC 3339 string with the timezone's offset
date_from_components(parts, tz?)parts: dict; tz: IANA timezone string (default UTC)int or floatBuild a Unix timestamp from {year, month, day, hour?, minute?, second?} interpreted in the given timezone
date_add(dt, duration)dt: float, int, or dict; duration: durationint or floatAdd a duration to a timestamp
date_diff(a, b)a, b: float, int, or dictdurationReturn the signed duration a - b
duration_ms(n)n: numberdurationCreate a duration from milliseconds
duration_seconds(n)n: numberdurationCreate a duration from seconds
duration_minutes(n)n: numberdurationCreate a duration from minutes
duration_hours(n)n: numberdurationCreate a duration from hours
duration_days(n)n: numberdurationCreate a duration from days
duration_to_seconds(duration)duration: durationintConvert a duration to whole seconds
duration_to_human(duration)duration: durationstringFormat a compact duration such as "3h 14m"
weekday_name(dt, tz?)dt: float, int, or dict; tz: IANA timezone stringstringWeekday name for a timestamp, optionally in a timezone
month_name(dt, tz?)dt: float, int, or dict; tz: IANA timezone stringstringMonth name for a timestamp, optionally in a timezone

Migration note: date_parse now tries standards-compliant RFC 3339 / ISO 8601 parsing first. Malformed strings that previously happened to work through digit extraction still fall back to that behavior, but impossible calendar dates such as "2024-02-31" now throw instead of rolling through timestamp arithmetic.

Vision

FunctionParametersReturnsDescription
vision_ocr(image, options?)image: string or dict, options: dictdictRun deterministic OCR over an image and return StructuredText with text, blocks, lines, tokens, source, backend, and stats. image may be a path string, {path, ...}, {storage: {path}, ...}, {bytes_base64, mime_type, name?}, or {data_url, name?}. options.language sets the Tesseract language code when the default backend is in use

Path-backed inputs obey the active handler workspace_roots read boundary. Audit events record image metadata and hashes, not raw image bytes.

Example:

import "std/vision"

let text = ocr("fixtures/receipt.png", {language: "eng"})
log(text.text)
log(text.tokens[0]?.text)
log(text.source.sha256)

Testing

FunctionParametersReturnsDescription
assert(condition, msg?)condition: any, msg: string (optional)nilAssert value is truthy. Throws with message on failure
assert_eq(a, b, msg?)a: any, b: any, msg: string (optional)nilAssert two values are equal. Throws with message on failure
assert_ne(a, b, msg?)a: any, b: any, msg: string (optional)nilAssert two values are not equal. Throws with message on failure

Test results

FunctionParametersReturnsDescription
parse_junit_xml(input)input: string or byteslist of dictParse a JUnit XML test report into per-case dicts. Lenient: malformed input returns [] instead of throwing

JUnit XML is the de facto interchange format for compiled-language test runners — GTest with --gtest_output=xml, Maven Surefire and Gradle for JUnit, xUnit/.NET — and is also emitted by pytest, vitest, and cargo-nextest's JUnit dialect. Each returned dict has the shape:

KeyTypeDescription
namestringFully qualified test name (classname::name when a classname attribute is present, else name alone)
statusstringOne of "passed", "failed", "skipped", "errored"
duration_msinttime attribute in seconds, converted to milliseconds
messagestring or nilConcatenation of any <failure> / <error> message attribute and child text
stdoutstring or nilCaptured <system-out> content
stderrstring or nilCaptured <system-err> content
pipeline summarize() {
  let xml = read_file("build/test-results/test-results.xml")
  let cases = parse_junit_xml(xml)
  let failed = cases.filter({ case -> case.status == "failed" || case.status == "errored" })
  log("failures: ${len(failed)} of ${len(cases)}")
  for case in failed {
    log("  ${case.name}: ${case.message}")
  }
}

HTTP

FunctionParametersReturnsDescription
http_get(url, options?)url: string, options: dictdictGET request
http_post(url, body, options?)url: string, body: string, options: dictdictPOST request
http_put(url, body, options?)url: string, body: string, options: dictdictPUT request
http_patch(url, body, options?)url: string, body: string, options: dictdictPATCH request
http_delete(url, options?)url: string, options: dictdictDELETE request
http_request(method, url, options?)method: string, url: string, options: dictdictGeneric HTTP request
http_download(url, dst_path, options?)url: string, dst_path: string, options: dictdictStream a response body to a file
egress_policy(config)config: dictdictInstall the process egress policy used by HTTP, SSE, WebSocket, and connector outbound calls
security_policy(config)config: dictdictInstall the prompt-injection defense policy (spotlighting of untrusted output + lethal-trifecta gate + MCP schema pinning + local-ml injection detection). See std/security.
http_server_tls_plain()nonedictBuild HTTP-server TLS config for intentional cleartext/local listener mode
http_server_tls_edge(options?)options: dictdictBuild HTTP-server TLS config for edge-terminated HTTPS; local listener stays plain and HSTS is enabled by default
http_server_tls_pem(cert_path, key_path)cert_path: string, key_path: stringdictBuild in-process HTTPS config from PEM files; missing files throw before startup
http_server_tls_self_signed_dev(hosts?)hosts: string or listdictGenerate a self-signed development cert/key config for local HTTPS testing. HSTS is disabled
http_server_security_headers(tls_config)tls_config: dictdictReturn TLS-aware response headers such as strict-transport-security; edge and PEM modes enable HSTS by default, plain and self-signed dev do not
http_session(options?)options: dictstringCreate a reusable host-managed HTTP client/session handle
http_session_request(session, method, url, options?)session: string, method: string, url: string, options: dictdictRun an HTTP request through a reusable session
http_session_close(session)session: stringboolClose a reusable HTTP session handle
http_stream_open(url, options?)url: string, options: dictstringOpen a streaming HTTP response handle
http_stream_read(stream, max_bytes?)stream: string, max_bytes: intbytes or nilRead the next response chunk
http_stream_info(stream)stream: stringdictReturn {status, headers, ok} for an open stream
http_stream_close(stream)stream: stringboolClose a streaming HTTP response handle
sse_connect(method, url, options?)method: string, url: string, options: dictstringOpen an SSE/Streamable HTTP receive handle
sse_receive(stream, timeout_ms?)stream: string, timeout_ms: intdict or nilReceive one SSE event with timeout/backpressure
sse_close(stream)stream: stringboolClose an SSE handle
sse_event(event, options?)event: any, options: dictstringFormat a server-sent event frame
sse_server_response(options?)options: dictdictCreate a text/event-stream response handle
sse_server_send(stream, event, options?)stream: string or dict, event: any, options: dictboolWrite one event frame to a server SSE response
sse_server_heartbeat(stream, comment?)stream: string or dict, comment: stringboolWrite an SSE comment/heartbeat frame
sse_server_flush(stream)stream: string or dictboolFlush pending server SSE frames when the client is still connected
sse_server_status(stream)stream: string or dictdictInspect buffered event count, close, cancel, and disconnect state
sse_server_disconnected(stream)stream: string or dictboolReturn whether the mock/client side disconnected
sse_server_cancelled(stream)stream: string or dictboolReturn whether the response was cancelled
sse_server_cancel(stream, reason?)stream: string or dict, reason: stringboolMark the response cancelled and closed
sse_server_close(stream)stream: string or dictboolClose a server SSE response
sse_server_mock_receive(stream)stream: string or dictdictDeterministically read the next buffered server SSE frame in tests
sse_server_mock_disconnect(stream)stream: string or dictboolSimulate a client disconnecting from a server SSE response
websocket_connect(url, options?)url: string, options: dictstringOpen a WebSocket client handle
websocket_server(bind?, options?)bind: string, options: dictdictStart a host-managed WebSocket server and return {id, addr, url}
websocket_route(server, path, options?)server: string or dict, path: string, options: dictboolRegister an HTTP upgrade route on a WebSocket server
websocket_accept(server, timeout_ms?)server: string or dict, timeout_ms: intdict or nilAccept one upgraded connection and return its socket handle plus peer metadata
websocket_send(socket, message, options?)socket: string, message: string or bytes, options: dictboolSend a WebSocket text/binary/ping/pong/close message
websocket_receive(socket, timeout_ms?)socket: string, timeout_ms: intdict or nilReceive one WebSocket message with timeout/backpressure
websocket_close(socket)socket: stringboolClose a WebSocket handle
http_server(options?)options: dictdictCreate an in-process inbound HTTP server definition for host adapters or synthetic tests
http_server_route(server, method, path_template, handler, options?)server: dict/string, method: string, path_template: string, handler: closure, options: dictdictRegister a route. Templates support {name} and :name path params
http_server_before(server, handler)server: dict/string, handler: closuredictRegister before middleware. Return a request to continue or a response dict to short-circuit
http_server_after(server, handler)server: dict/string, handler: closuredictRegister after middleware. Receives (response, request) and may return a replacement response
http_server_request(server, request)server: dict/string, request: dictdictDispatch a synthetic or host-adapted request through the server
http_server_test(server, request)server: dict/string, request: dictdictAlias for http_server_request, intended for script-level tests
http_server_set_ready(server, ready)server: dict/string, ready: boolboolSet the server readiness gate used by request dispatch
http_server_readiness(server, handler)server: dict/string, handler: closuredictRegister a readiness callback for http_server_ready
http_server_ready(server)server: dict/stringboolReturn readiness, invoking the readiness callback when present
http_server_on_shutdown(server, handler)server: dict/string, handler: closuredictRegister a shutdown lifecycle callback
http_server_shutdown(server)server: dict/stringboolMark the server shut down and run shutdown callbacks
http_response(status, body?, headers?)status: int, body: any, headers: dictdictBuild a response dict
http_response_text(text, options?)text: any, options: dictdictBuild a text response. Options include status and headers
http_response_json(value, options?)value: any, options: dictdictBuild a JSON response with a JSON content type
http_response_bytes(bytes, options?)bytes: bytes/string, options: dictdictBuild a bytes response
http_header(headers_or_message, name)headers/request/response: dict, name: stringstring or nilRead a header case-insensitively from a header dict, request, or response
websocket_server_close(server)server: string or dictboolStop a WebSocket server handle

http_get/post/put/patch/delete/request/session_request return {status: int, headers: dict, body: string, ok: bool, final_url: string}. http_download returns {bytes_written, status, headers, ok}. Options: timeout_ms (alias timeout, both in ms), total_timeout_ms, connect_timeout_ms, read_timeout_ms, retry: {max, backoff_ms}, legacy aliases retries / backoff, optional retry_on (status list), optional retry_methods (defaults to GET, HEAD, PUT, DELETE, OPTIONS), headers (dict), query (dict; nil values omitted and other values stringified), auth (string or {bearer: "token"} or {basic: {user, password}}), follow_redirects (bool), max_redirects (int), body (string), multipart (list<{name, value|value_base64|path, filename?, content_type?}>), proxy (string or {url, no_proxy?}), proxy_auth ({user, pass}), tls ({ca_bundle_path?, client_cert_path?, client_key_path?, client_identity_path?, pinned_sha256?}), and decompress (bool, default true). timeout_ms and total_timeout_ms apply per attempt. Retryable responses default to 408, 429, 500, 502, 503, and 504; Retry-After is honored on 429 and 503 when retries are enabled. Throws on network errors. http_request(..., {session: handle}) routes through an existing session when one is provided. http_post, http_put, and http_patch accept an options dict as the second argument when you want to send multipart without a separate string body.

egress_policy({allow, deny, default}) installs a process-scoped outbound network policy before user code opens real connections. Rules accept exact hosts (api.example.com), suffix wildcards (*.example.com), IP literals or CIDR ranges (127.0.0.0/8), and optional port restrictions (api.example.com:443). Deny rules override allow rules; default: "deny" turns the policy into an allowlist. Operators can seed the same policy without editing scripts via comma-separated HARN_EGRESS_ALLOW, HARN_EGRESS_DENY, and HARN_EGRESS_DEFAULT=deny. Under default harn run, the worktree sandbox denies network side effects before destination policy is consulted; use egress_policy(...) for network-enabled host policies or explicit --no-sandbox runs.

pipeline main(task) {
  egress_policy({
    allow: ["api.example.com:443", "*.trusted.example", "10.0.0.0/8"],
    deny: ["blocked.trusted.example"],
    default: "deny",
  })

  let response = http_get("https://api.example.com/v1/status")
  log(response.status)
}

Blocked attempts throw {type: "EgressBlocked", category: "egress_blocked", host, port, reason, url} and append an egress.blocked event to connectors.egress.audit when an event log is active. The same policy is checked by http_request and friends, http_session_request, http_stream_open, http_download, sse_connect, websocket_connect, and Rust-backed connector_call clients.

http_stream_open uses the same request options as http_request. The returned handle can be inspected with http_stream_info, drained with repeated http_stream_read(stream, max_bytes), and closed explicitly with http_stream_close. Reads return bytes; once the stream is exhausted they return nil.

std/web

Import deterministic web-source helpers with import { ... } from "std/web". They reuse the normal Harn HTTP stack, so egress policy, proxies, TLS options, sessions, retries, and http_mock apply exactly as they do for http_request. Sensitive request headers and query params remain governed by the HTTP mock redaction rules; std/web adds source provenance but does not log response bodies by itself.

FunctionArgsReturnsDescription
web_fetch(url, options?)url: string, options: dictdictFetch a source and return {ok, status, body, headers, content_type, etag, last_modified, fetched_at, cache_status, source_url, final_url, not_modified}
web_search(query, options?)query: string, options: dictdictSearch curated docs/registry entries, a configured JSON API, provider-hosted result captures, or HARN_WEB_SEARCH_URL; returns normalized results with per-result provenance
verify_imports(paths, options?)paths: string or list, options: dictdictVerify imports in source files against manifests, installed-package hints, registry evidence, symbol metadata, and package trust signals
web_grounding_tools(registry?, options?)registry: tool registry or nil, options: dicttool registryAdd read-only web_search and verify_imports tools with capability-gated model guidance
web_parse_html(html, source_url?)html: string, source_url: string or nildictExtract {title, meta, canonical_url, links, tables, json_ld, text} from deterministic HTTP-fetched HTML
web_resolve_url(base_url, href)base_url: string, href: stringstring or nilResolve a relative URL reference against a source URL
web_origin_url(url, path?)url: string, path: stringstringReturn the URL origin with a replacement path and no query or fragment. Relative paths are rooted at /
robots_allowed(url, user_agent?, options?)url: string, user_agent: string, options: dictboolCheck robots.txt using mocked or real HTTP. Missing/non-2xx robots files allow by default
sitemap_urls(base_url, options?)base_url: string, options: dictlist of stringsDiscover URLs from robots-advertised sitemaps or /sitemap.xml
html_title/html_meta/html_links/html_tables/html_json_ld/html_texthtml: string, source_url?: stringvalueConvenience projections over web_parse_html

web_fetch accepts normal http_request options directly, or an explicit http_options dict. Passing store from std/cache enables recurring conditional fetches: cached ETag and Last-Modified values become If-None-Match and If-Modified-Since request headers, and a 304 response returns the cached body with cache_status: "not_modified". Pass previous to use a prior envelope without a cache store, cache_key to override the cache key, conditional: false to suppress conditional headers, and fetched_at to pin deterministic fixture timestamps. The default cache key is method plus the effective request URL after query options are applied, so distinct query variants do not share a cached envelope.

web_search is provider-agnostic. Pass index or results for deterministic curated entries, api for a JSON search service fetched through web_fetch, provider_results to normalize externally captured hosted-search results, or configure HARN_WEB_SEARCH_URL for process-level search. Each result includes title, url, snippet, source_url, ranking metadata, and a provenance record with backend id, evidence type, score, source URL, and trust metadata when available. Result envelopes expose only public backend metadata; configured API request headers and bodies are not echoed back.

verify_imports scans Python, JavaScript/TypeScript, Rust, and Harn imports. It resolves packages from nearby package.json, pyproject.toml, requirements*.txt, and Cargo.toml manifests, optional installed_packages, and optional registry entries with symbols, source_url, trust_score, first_seen, or published_at. The return status is pass, warn, or fail; package_not_found and symbol_not_found are blocking unresolved findings, while low_trust_package, fresh_package, and symbol_unverified are warnings intended to force a lookup before coding continues.

robots_allowed implements a deterministic robots subset for source-ingest workflows: exact user-agent groups take precedence over wildcard groups, multiple matching groups at the same specificity are combined, and Allow/Disallow decisions use longest-prefix matching with Allow winning ties.

import { mem_cache } from "std/cache"
import { verify_imports, web_fetch, web_parse_html, web_search, robots_allowed, sitemap_urls } from "std/web"

let store = mem_cache({namespace: "weekly-doc-monitor"})
let page = web_fetch("https://docs.example.com/models", {store: store})
if page.cache_status != "not_modified" && robots_allowed(page.final_url) {
  let parsed = web_parse_html(page.body, page.final_url)
  log(parsed.title)
  log(sitemap_urls(page.final_url))
}

let docs = web_search("fastapi dependency injection", {
  index: [
    {
      title: "FastAPI Dependencies",
      url: "https://fastapi.tiangolo.com/tutorial/dependencies/",
      source_type: "docs",
      authority: true,
    },
  ],
})
let imports = verify_imports("app.py", {
  registry: [{ecosystem: "python", name: "fastapi", symbols: ["FastAPI"]}],
})

HTTP server TLS helper builtins only describe listener/security policy. Runtime hosts such as harn serve consume the same modes: plain for deliberate cleartext, edge when a proxy/load balancer terminates public TLS, pem for in-process HTTPS with certificate/key files, and self_signed_dev for local HTTPS testing. http_server_security_headers(...) emits HSTS for edge and PEM configs so edge-terminated deployments can still set browser-facing security headers from the Harn layer; it deliberately omits HSTS for plain and self-signed dev configs.

Transport handles are strings owned by the VM host. Rust keeps responsibility for TCP/TLS/socket lifecycle, HTTP pooling, HTTP-to-WebSocket upgrade handling, SSE/WebSocket protocol parsing, backpressure, receive timeouts, cancellation by dropping/closing handles, and resource limits. Connector packages should use sse_receive, websocket_accept, and websocket_receive as pull-based loops; each call reads at most one event/message and returns {type: "timeout"} on timeout or nil after close.

SSE events return {type: "open"} or {type: "event", event, data, id, retry_ms}. WebSocket receives return {type: "text", data}, {type: "binary", data_base64}, {type: "ping", data_base64}, {type: "pong", data_base64}, {type: "close", code?, reason?}, or {type: "timeout"}. Options include max_events/max_messages and max_message_bytes. WebSocket server route options also include auth: {bearer: "token"} and idle_timeout_ms; unauthorized or unregistered upgrade paths are rejected during the HTTP upgrade. Server outbound backpressure is explicit: send_buffer_messages bounds queued server-to-client frames, and websocket_send throws when that queue is full. websocket_connect accepts headers and auth: {bearer: "token"} options for clients that need upgrade metadata.

Minimal inbound echo:

pipeline websocket_echo() {
  let server = websocket_server("127.0.0.1:8787", {})
  websocket_route(server, "/acp", {auth: {bearer: env("ACP_TOKEN")}})

  while true {
    let accepted = websocket_accept(server, 30000)
    if accepted == nil || accepted?.type == "timeout" {
      continue
    }
    let conn = accepted ?? {}

    let frame = websocket_receive(conn, 30000) ?? {}
    if frame?.type == "text" {
      websocket_send(conn, frame.data, {})
    }
    websocket_close(conn)
  }
}

Inbound HTTP server primitives

The server builtins define a Harn-native request router without binding a socket themselves. A host adapter can translate real HTTP requests into http_server_request(...); tests can use the same path with http_server_test(...).

Requests passed to route handlers include:

  • method, path, path_params/params, query, and normalized lowercase headers
  • body as text plus raw_body bytes when retained
  • body_bytes, remote_addr, and client_ip

http_server({max_body_bytes, retain_raw_body, ready}) sets defaults. Routes can override max_body_bytes and retain_raw_body. Body-limit rejections return status 413 before middleware or handlers run.

Minimal webhook example:

pipeline default() {
  let server = http_server({max_body_bytes: 1048576, retain_raw_body: true})

  http_server_before(server, { req ->
    if http_header(req, "origin") != nil {
      return http_response_text("browser origins are rejected", {status: 403})
    }
    req
  })

  http_server_after(server, { response, _req ->
    response + {
      headers: response.headers + {
        ["strict-transport-security"]: "max-age=31536000",
      },
    }
  })

  http_server_route(server, "POST", "/hooks/{tenant}/{trigger}", { req ->
    let signature = http_header(req, "x-hub-signature-256")
    let expected = "sha256=" + hmac_sha256(secret_get("github/webhook-secret"), req.body)
    if signature != expected {
      return http_response_text("invalid signature", {status: 401})
    }

    let payload = json_parse(req.body)
    trigger_fire("github-webhook", {
      tenant: req.path_params.tenant,
      trigger: req.path_params.trigger,
      payload: payload,
      raw_body: req.raw_body,
      client_ip: req.client_ip,
    })
    http_response_json({accepted: true}, {status: 202, headers: {["retry-after"]: "0"}})
  })

  let probe = http_server_test(server, {
    method: "POST",
    path: "/hooks/acme/push",
    headers: {["x-hub-signature-256"]: "sha256=..."},
    body: "{\"ok\":true}",
    client_ip: "203.0.113.10",
  })
  log(probe.status)
}

Server-side SSE primitives

Server-side SSE responses are VM-owned handles. sse_server_response() returns {id, type: "sse_response", status, headers, body: nil, streaming: true} with content-type: text/event-stream; charset=utf-8, cache-control: no-cache, connection: keep-alive, and x-accel-buffering: no unless overridden. sse_server_send() formats fields as UTF-8 SSE lines: event, id, retry or retry_ms, and multi-line data. sse_server_heartbeat() writes comment frames. max_event_bytes rejects oversized frames before buffering, and max_buffered_events rejects writes when the client is not draining quickly enough. sse_server_flush() reports whether the stream is still writable after marking currently buffered events flushed. Writes return false after close, cancel, or disconnect. Use sse_server_status(), sse_server_disconnected(), and sse_server_cancelled() to observe shutdown state.

pipeline progress_stream(task) {
  let stream = sse_server_response({max_event_bytes: 4096})
  sse_server_send(stream, {event: "progress", id: "1", data: "queued"})
  sse_server_heartbeat(stream, "still working")
  sse_server_send(stream, {event: "progress", id: "2", data: "done"})
  sse_server_flush(stream)
  return stream
}

Mock HTTP

For testing pipelines that make HTTP calls without hitting real servers.

FunctionParametersReturnsDescription
http_mock(method, url_pattern, response)method: string, url_pattern: string, response: dictnilRegister a mock. Use * in url_pattern for glob matching (supports multiple * wildcards, e.g., https://api.example.com/*/items/*). response may be a single {status, body, headers} dict or {responses: [...]} to script retries.
http_mock_clear()nonenilClear all mocks and recorded calls
http_mock_calls(options?)options: dictlistReturn list of {method, url, headers, body} for all intercepted calls. Header names are normalized to lowercase. Sensitive request headers and query parameters are redacted by default; pass {redact_sensitive: false} or {include_sensitive: true} to inspect raw values in tests.
sse_mock(url_pattern, events_or_config)url_pattern: string, events_or_config: list or dictnilRegister an in-process SSE stream mock. Events may be strings or {event, data, id?, retry_ms?} dicts.
websocket_mock(url_pattern, messages_or_config)url_pattern: string, messages_or_config: list or dictnilRegister an in-process WebSocket mock. Messages may be strings/bytes or {type, data} dicts; {messages: [...], echo: true} enables echoing sends.
transport_mock_calls()nonelistReturn recorded mocked SSE/WebSocket connect/send/close calls
transport_mock_clear()nonenilClear mocked SSE/WebSocket transports and recorded calls
http_mock("GET", "https://api.example.com/users", {
  responses: [
    {status: 429, headers: {"retry-after": "0"}},
    {status: 200, body: "{\"users\": [\"alice\"]}", headers: {}},
  ]
})
let resp = http_get("https://api.example.com/users", {
  retry: {max: 1, backoff_ms: 0}
})
assert_eq(resp.status, 200)

Mocks match and record the final request URL after query options are appended:

http_mock("GET", "https://api.example.com/users?limit=2", {status: 200})
http_get("https://api.example.com/users", {
  headers: {Authorization: "Bearer test-token"},
  query: {limit: 2},
})
let call = http_mock_calls({redact_sensitive: false})[0]
assert_eq(call.url, "https://api.example.com/users?limit=2")
assert_eq(call.headers.authorization, "Bearer test-token")
let stream = http_stream_open("https://example.com/archive.tar.gz", {
  decompress: false,
  connect_timeout_ms: 5000,
  read_timeout_ms: 30000,
})
let meta = http_stream_info(stream)
let chunk = http_stream_read(stream, 65536)
http_stream_close(stream)

Postgres

FunctionParametersReturnsDescription
pg_pool(source, options?)source: string or dict, options: dictdictOpen a pooled Postgres connection
pg_connect(source, options?)source: string or dict, options: dictdictOpen a single-connection Postgres pool
pg_query(handle, sql, params?)handle: dict, sql: string, params: listlistRun a parameterized query and return decoded rows
pg_query_one(handle, sql, params?)handle: dict, sql: string, params: listdict or nilReturn the first decoded row, or nil when no row matches
pg_execute(handle, sql, params?)handle: dict, sql: string, params: listdictExecute a parameterized statement and return {rows_affected}
pg_transaction(pool, callback, options?)pool: dict, callback: closure, options: dictanyRun a closure with a transaction handle, commit on success, rollback on throw
pg_close(pool)pool: dictboolClose and unregister a pool
pg_stmt_cache_clear(pool)pool: dictdictClear prepared-statement caches on idle primary and replica connections
pg.jsonb.path(pool, document, jsonpath)pool: dict, document: any, jsonpath: stringlistRun jsonb_path_query with bound operands
pg.jsonb.merge(pool, left, right)pool: dict, left: any, right: anyanyMerge two JSONB values with Postgres || semantics
pg.jsonb.contains(pool, left, right)pool: dict, left: any, right: anyboolTest JSONB containment with Postgres @> semantics
pg_mock_pool(fixtures)fixtures: listdictCreate a fixture-backed Postgres handle for tests
pg_mock_calls(mock)mock: dictlistReturn recorded mock SQL calls

Connection sources may be raw Postgres URLs, env:NAME, secret:namespace/name, or {url}, {env}, or {secret} dictionaries. Pool options include max_connections, min_connections, acquire_timeout_ms, idle_timeout_ms, max_lifetime_ms, ssl_mode, application_name, and statement_cache_capacity. Pool options also accept replicas and read_routing_policy.

pg_stmt_cache_clear(pool) returns {pools, connections_cleared, connections_skipped}. It does not close or recreate the pool; checked-out connections are skipped so in-flight queries are not interrupted.

Use params for every dynamic value:

let rows = pg_query(
  db,
  "select id, payload from receipts where tenant_id = $1 and id = $2::uuid",
  [tenant_id, receipt_id],
)

Rows decode into dictionaries. JSON/JSONB becomes Harn values; UUID, date, time, timestamp, and timestamptz decode as strings; hstore, ranges, and geometric types decode as structured dictionaries. Transaction options may include settings, which are applied with transaction-local set_config for RLS policies:

pg_transaction(db, { tx ->
  pg_execute(tx, "insert into event_log(tenant_id, kind) values ($1, $2)", [
    tenant_id,
    "receipt.created",
  ])
}, {settings: {"app.current_tenant_id": tenant_id}})

See Postgres for the full persistence guide and mock fixture examples.

Interactive input

FunctionParametersReturnsDescription
read_stdin()string or nilRead the remaining stdin contents
is_stdin_tty() / is_stdout_tty() / is_stderr_tty()boolReturn whether the corresponding process stream is attached to a terminal

Ambient stdio helpers were removed. Use harness.stdio.print, harness.stdio.println, harness.stdio.eprint, harness.stdio.eprintln, harness.stdio.read_line, and harness.stdio.prompt from code that already has a Harness handle. Use harness.term.width(), harness.term.height(), and harness.term.read_password(prompt?) for terminal dimensions and no-echo password input. For structured interactive input outside a harness entrypoint, import read_line, read_password, is_tty, and write_stderr from std/io.

Host interop

FunctionParametersReturnsDescription
host_call(name, args)name: string, args: anyanyCall a host capability operation using capability.operation naming
host_capabilities()dictTyped host capability manifest
host_has(capability, op?)capability: string, op: stringboolCheck whether a typed host capability/operation exists
host_tool_list()listList host-exposed bridge tools as {name, description, schema, deprecated}
host_tool_call(name, args)name: string, args: anyanyInvoke a bridge-exposed host tool by name using the existing builtin_call path
host_mock(capability, op, response_or_config, params?)capability: string, op: string, response_or_config: any or dict, params: dictnilRegister a runtime mock for a typed host operation
host_mock_clear()nilClear registered typed host mocks and recorded mock invocations
host_mock_calls()listReturn recorded typed host mock invocations

host_capabilities() returns the capability manifest surfaced by the active host bridge. The local runtime exposes generic process, template, and interaction capabilities. Product hosts can add capabilities such as workspace, project, runtime, editor, git, or diagnostics.

The process capability includes the shell discovery contract used by shell-mode command runners:

  • process.list_shells returns {shells, default_shell_id} with stable shell IDs, paths, platform, availability, invocation args, and source.
  • process.get_default_shell returns the selected shell object for the current host/session.
  • process.set_default_shell selects a shell by shell_id for stateful hosts.
  • process.shell_invocation resolves {shell_id?, shell?, command?, login?, interactive?} to {program, args, command_arg_index, shell}. When neither shell_id nor shell is supplied, it uses the selected default shell.

Programmatic execution should prefer argv mode (process.exec with mode: "argv"). Shell mode is for user-authored commands; callers can pass a shell object or shell ID discovered through this capability, or omit both to use the selected default shell.

Prefer host_call("capability.operation", args) in shared wrappers and host-owned .harn modules so capability names stay consistent across the runtime, host manifest, and preflight validation.

host_tool_list() is the discovery surface for host-native tools such as Read, Edit, Bash, or IDE actions exposed by the active bridge host. Without a bridge it returns []. host_tool_call(name, args) uses that same bridge host's existing dynamic builtin dispatch path, so scripts can discover a tool at runtime and then call it by name without hard-coding it into the initial prompt. Import std/host when you want small helpers such as host_tool_lookup(name) or host_tool_available(name).

host_mock(...) is intended for tests and local conformance runs. The third argument may be either a direct result value or a config dict containing result, params, and/or error. Mock matching is last-write-wins and only requires the declared params subset to match the actual host call call. Matched calls are recorded in host_mock_calls() as {capability, operation, params} dictionaries.

For higher-level test helpers, import std/testing:

import {
  assert_host_called,
  clear_host_mocks,
  mock_host_error,
  mock_host_result,
} from "std/testing"

clear_host_mocks()
mock_host_result("project", "metadata_get", "hello", {dir: ".", namespace: "facts"})
assert_eq(host_call("project.metadata_get", {dir: ".", namespace: "facts"}), "hello")
assert_host_called("project", "metadata_get", {dir: ".", namespace: "facts"}, nil)

mock_host_error("project", "scan", "scan failed", nil)
let result = try { host_call("project.scan", {}) }
assert(is_err(result))

std/testing also includes persona steel-thread assertions: step_assertions_begin, step_events, assert_steps_ran, assert_step_received, assert_step_emitted, assert_handoff_emitted, assert_receipt_field, and assert_golden_transcript. These helpers record existing PreStep / PostStep hook payloads and assert Harn-level step, handoff, receipt, and structured transcript boundaries.

Git stdlib

The git namespace provides typed local repository operations over the runtime command runner. These are local subprocess operations, not forge connector calls. Every operation returns a harn-stdlib-git-receipt-v1 envelope with command args, working directory, status, exit category, output refs, affected paths, agent identity, trace id, command-policy audit data, and operation data. Receipts are appended to stdlib.git.receipts; each operation also writes a TrustGraph record.

FunctionParametersReturnsDescription
git.status(repo)repo: path string or repo dictGitReceiptRun git status --porcelain=v1 --branch and return structured entries
git.conflicts(repo)repo: path string or repo dictGitReceiptReturn structured unmerged paths and conflict kinds where git exposes them
git.fetch(repo, remote, refspecs)repo: path/dict, remote: string, refspecs: list of stringsGitReceiptFetch from an existing local remote configuration
git.rebase(repo, base_ref)repo: path/dict, base_ref: stringGitReceiptRebase the current branch onto base_ref; treated as risky for approval/autonomy
git.push(repo, remote, refspec, lease?)repo: path/dict, remote: string, refspec: string, lease: {ref?, expected_oid} or oid stringGitReceiptPush a refspec. Force-with-lease requires an expected remote OID and fails with lease_mismatch if the remote advanced
git.diff(repo, selector?)selector: range string, path list, or {range?, paths?}GitReceiptReturn diff text for a range and/or paths
git.merge_base(repo, left, right)left/right: refsGitReceiptReturn the merge-base OID
git.repo_discover(path)path: stringGitReceiptDiscover repository root/git-dir metadata
git.worktree_create(repo, branch, path, options?)options: {base_ref?, force?, detach?}GitReceiptCreate a worktree using argv-mode git
git.worktree_remove(path, options?)options: {force?}GitReceiptRemove a worktree; missing paths return an idempotent status: "no_op" receipt

Canonical builtin names are also registered as git.repo.discover, git.worktree.create, and git.worktree.remove. The root aliases above are the ergonomic Harn call surface for nested operations.

Import std/git for first-class Harn function wrappers around this namespace plus local argv-mode helpers that are suitable as granular agent tools. The receipt-producing wrappers (git_status, git_diff, git_rebase, git_push, and worktree helpers) delegate to the audited builtins above. The local command helpers (git_run, git_current_branch, git_log, git_switch, git_pull_ff_only, git_fetch, git_branch_list, and git_remote_list) run git through argv-mode process.exec with ambient git environment overrides removed.

git_tools(registry?, options?) builds a selected tool registry from those functions. It defaults to read-only git inspection helpers and only exposes checkout-changing helpers such as git_switch or git_pull_ff_only when they are explicitly included in enabled_tools. It accepts the same Tool Vault metadata knobs used by host tool helpers: defer_loading, namespace, and per-tool tool_config.

For small/local models, git_toolbox_tools(registry?, options?) exposes a compact two-tool surface: find_git_tool ranks available git operations with a deterministic lexical scorer, and run_git_tool executes one returned operation id. Mutating operations stay unavailable unless the toolbox is configured with include_mutations: true.

Command policy

FunctionParametersReturnsDescription
command_policy(config)config: dictdictNormalize a command-runner policy with workspace roots, deterministic deny/approval rules, and optional pre/post closures
command_policy_push(policy)policy: dictnilInstall a command policy for the current VM scope
command_policy_pop()nilRemove the most recently installed command policy
with_autonomy_policy(policy, fn)policy: dict, fn: closurewhatever fn returnsRun fn with a scoped autonomy tier policy; side-effecting builtins are enforced by the VM
with_execution_policy(policy, fn)policy: dict, fn: closurewhatever fn returnsRun fn with a scoped capability policy; the policy is popped on success or throw
with_approval_policy(policy, fn)policy: dict, fn: closurewhatever fn returnsRun fn with a scoped tool approval policy; the policy is popped on success or throw
with_command_policy(policy, fn)policy: dict, fn: closurewhatever fn returnsRun fn with a scoped command policy; the policy is popped on success or throw
with_dynamic_permissions(policy, fn)policy: dict, fn: closurewhatever fn returnsRun fn with a scoped dynamic permission policy; the policy is popped on success or throw
command_risk_scan(ctx)ctx: dictdictRun deterministic command-risk classification and return labels, confidence, rationale, and recommended action
command_result_scan(ctx)ctx: dictdictClassify a command result envelope for unsafe output or audit annotations
command_llm_risk_scan(ctx, options?)ctx: dict, options: dictdictReturn the structured risk-scan helper shape with redacted options; deterministic fallback does not make network calls

Install policies directly or pass them to agent_loop with command_policy: policy / policy: {command_policy: policy}. Active policies wrap host_call("process.exec", ...): pre-hooks run before spawn, blocked decisions return a status: "blocked" envelope without starting a child process, post-hooks can annotate the command audit, and hook recursion is denied unless allow_recursive: true.

Async and timing

FunctionParametersReturnsDescription
sleep(duration)duration: int (ms) or duration literalnilPause execution

Durable agent channels

Durable channels publish structured agent/runtime facts into the active event log instead of an in-process channel(...) handle. Use them when a fact should be visible to trigger orchestration, later workers, or audit readers.

FunctionParametersReturnsDescription
emit_channel(name, payload, options?)name: string, payload: any, options: {id?: string, scope?: "session" | "pipeline" | "tenant" | "org", tenant_id?: string, session_id?: string, pipeline_id?: string, ttl?: duration}dictAppend a durable channel event and return {event_id, id, name_resolved, scope, scope_id, emitted_at, emitted_by, retention, duplicate}. Bare names default to tenant scope. Reusing the same id on the same resolved channel is a no-op and returns the original event_id.
channel_events(name, options?)name: string, options: {scope?: string, from_cursor?: int, cursor?: int, limit?: int}listRead stored channel events for the resolved channel, oldest first. Intended for tests, diagnostics, and local orchestration inspection.

Channel names resolve to scope:scope_id:name. Bare emit_channel("pr.merged", payload) resolves to tenant:<current-or-default-tenant>:pr.merged. Prefixes select a scope: session:foo, pipeline:foo, tenant:foo, tenant:<tenant_id>:foo, and org:<org_id>:foo. Session events are retained in the current process, pipeline and tenant events use the active durable event log, and org-scoped channels currently fail with HARN-CHN-002 until org grants are available. Every stored event includes a signed emitted_at timestamp, emitted_by, the fully resolved name, any available pipeline_id, session_id, or tenant_id, and ttl_ms when options.ttl is provided.

The resolver enforces a four-tier hierarchy (session < pipeline < tenant < org) deterministically. Cross-scope isolation is automatic: distinct tenant_id, session_id, or pipeline_id values resolve to distinct topics, so a reader against a different scope id sees an empty view. Diagnostic codes emitted by the resolver:

CodeWhen
HARN-CHN-001pipeline: scope used outside any pipeline context.
HARN-CHN-002Cross-tenant emit without a grant, or org: scope (disabled in v1).
HARN-CHN-003Malformed channel name, scope prefix, or scope id.
HARN-CHN-004Scope ambiguous — explicit options.session_id or options.pipeline_id conflicts with the active runtime context.

Concurrency primitives

Channels

FunctionParametersReturnsDescription
channel(name?)name: string (default "default")dictCreate a channel with name, type, and messages fields
send(ch, value)ch: channel, value: anyboolSend a value to a channel. Throws ChannelClosed after close
receive(ch)ch: channelanyReceive a value from a channel. Blocks until data is available; after close, drains buffered values then throws ChannelClosed
close_channel(ch)ch: channelnilClose a channel, wake waiters, and prevent further sends
try_receive(ch)ch: channelany or nilNon-blocking receive. Returns nil if no data available
select(ch1, ch2, ...)channels: channeldict or nilWait for data on any channel. Returns {index, value, channel} for the first ready channel, or nil if all closed
channel_select(channels, timeout?)channels: list[channel], timeout: int or durationdict or nilSelect over a channel list with an optional timeout

Supervisors

FunctionParametersReturnsDescription
supervisor_start(spec)spec: dictsupervisor handleStart a named supervisor with child task closures, child kinds, restart policy, and propagation strategy
supervisor_state(handle_or_id)handle or stringdictReturn supervisor children, status, restart counts, last errors, wait reasons, active leases, next restart times, and metrics
supervisor_events(handle_or_id)handle or stringlistReturn lifecycle events for started, stopped, failed, restarted, suppressed, escalated, and shutdown activity
supervisor_metrics(handle_or_id)handle or stringdictReturn aggregate lifecycle counters
supervisor_stop(handle_or_id, timeout?)handle or string, durationdictRequest cooperative child cancellation, wait for drain, then force-abort remaining children

Atomics

FunctionParametersReturnsDescription
atomic(initial?)initial: any (default 0)dictCreate an atomic value
atomic_get(a)a: dictanyRead the current value
atomic_set(a, value)a: dict, value: anyintSet value, returns previous value
atomic_add(a, delta)a: dict, delta: intintAdd delta, returns previous value
atomic_cas(a, expected, new)a: dict, expected: int, new: intboolCompare-and-swap. Returns true if the swap succeeded

Persistent store

FunctionParametersReturnsDescription
store_get(key)key: stringanyRetrieve value from store, nil if missing
store_set(key, value)key: string, value: anynilStore value, auto-saves to .harn/store.json
store_delete(key)key: stringnilRemove key from store
store_list()nonelistList all keys (sorted)
store_save()nonenilExplicitly flush store to disk
store_clear()nonenilRemove all keys from store

The store is backed by .harn/store.json relative to the script's directory. The file is created lazily on first store_set. In bridge mode, the host can override these builtins.

LLM

See LLM calls and agent loops for full documentation.

FunctionParametersReturnsDescription
llm_call(prompt, system?, options?)prompt: string, system: string, options: dictdictSingle LLM request. Returns {text, model, provider, input_tokens, output_tokens, usage, prose, visible_text, blocks, transcript?, tool_calls?, stop_reason?, data?}. Supports budget: {max_cost_usd?, max_input_tokens?, max_output_tokens?, total_budget_usd?} pre-flight checks and throws on transport / rate-limit / budget / schema-validation failures
llm_call_safe(prompt, system?, options?)prompt: string, system: string, options: dictdictNon-throwing envelope around llm_call. Returns {ok: bool, response: llm_call result or nil, error: {category, kind, reason, message, provider?, model?, status?, retry_after_ms?} or nil}. error.category is one of ErrorCategory's canonical strings ("rate_limit", "timeout", "overloaded", "server_error", "transient_network", "schema_validation", "auth", "not_found", "circuit_open", "budget_exceeded", "tool_error", "tool_rejected", "egress_blocked", "cancelled", "generic")
llm_stream_call(prompt, system?, options?)prompt: string, system: string, options: dictstreamStreaming LLM request. Returns Stream<{delta, visible_delta, partial, role, finish_reason}>; dropping the stream cancels the background request. Uses the same options as llm_call; the stream option remains the transport toggle
with_rate_limit(provider, fn, options?)provider: string, fn: closure, options: dictwhatever fn returnsAcquire a permit from the provider's sliding-window rate limiter, invoke fn, and retry with exponential backoff on retryable errors (rate_limit, overloaded, transient_network, timeout). Options: max_retries (default 5), backoff_ms (default 1000, capped at 30s after doubling)
llm_completion(prefix, suffix?, system?, options?)prefix: string, suffix: string, system: string, options: dictdictText completion / fill-in-the-middle request. Returns the same result shape as llm_call
agent_loop(prompt, system?, options?)prompt: string, system: string, options: dictdictMulti-turn agent loop with natural completion for native-tool loops, cooperative worker suspension (status: "suspended" with a resumable handle), sentinel completion for text/no-tool loops (<done>##DONE##</done> in tagged text-tool stages), daemon/idling support, optional per-turn context filtering, session-local scratchpad recitation/reorganization, opt-in repeated-tool-call stall diagnostics, and structured provider/tool-protocol failure capture. Returns {status, error?, text, visible_text, llm: {iterations, duration_ms, input_tokens, output_tokens}, tools: {calls, successful, rejected, mode}, transcript, task_ledger, trace, …}
agent_progress(input)input: dictnilEmit a progress_reported agent event for the current session. input requires either message: string or entries: [{content, status, priority?}]; replace defaults to true, and metadata defaults to {}
agent_turn(prompt, options?)prompt: string, options: dictdictHigh-level agent turn wrapper around agent_loop. It installs generic user-visible progress guidance, requires the completion judge (done_judge), defaults to loop-until-done completion (natural for native-tool turns, sentinel-based for text/no-tool turns), and returns the normal loop result plus iterations and judge_decisions summaries
agent_llm_turn(prompt, system?, options?)prompt: string, system: string, options: dictdictLow-level one-turn LLM request used by stdlib orchestration; equivalent to llm_call but intentionally lives under the agent primitive surface
agent_parse_tool_calls(text, tools?)text: string, tools: registry or nildictParse tagged/text-mode tool calls into {tool_calls, prose, canonical_text, protocol_violations, tool_parse_errors, done_marker}
agent_dispatch_tool_call(call, tools?, options?)call: dict, tools: registry or nil, options: dictdictDispatch one normalized tool call through the runtime parser/enforcement path and return {ok, status, rendered_result, error_category, executor, ...}
agent_dispatch_tool_batch(calls, tools?, options?)calls: list, tools: registry or nil, options: dictlistDispatch a list of normalized tool calls through the host batch primitive and return one result envelope per call; a leading read-only run may execute in parallel
daemon_spawn(config)config: dictdictStart a daemon-mode agent and return a daemon handle with persisted state + queue metadata
daemon_trigger(handle, event)handle: dict or string, event: anydictEnqueue a durable FIFO trigger event for a running daemon; throws VmError::DaemonQueueFull on overflow
daemon_snapshot(handle)handle: dict or stringdictReturn the latest daemon snapshot plus live queue state (pending_events, inflight_event, counts, capacity)
daemon_stop(handle)handle: dict or stringdictStop a daemon and preserve queued trigger state for resume
daemon_resume(path)path: stringdictResume a daemon from its persisted state directory
external_agent_delegate(target, task, options?)target: string, task: string, options: dictdictDelegate to an open A2A external agent using harn.external_agent.v1; returns a checkpoint envelope before dispatch, enforces hard budget/idempotency capability checks, and normalizes completed work into reviewable handoff/diff artifacts
trigger_list()listReturn the live trigger registry snapshot as list<TriggerBinding>
trigger_register(config)config: dictdictDynamically register a trigger and return its TriggerHandle
trigger_fire(handle, event)handle: dict or string, event: dictdictFire a synthetic event into a trigger and return a DispatchHandle; execution routes through the trigger dispatcher
trigger_replay(event_id)event_id: stringdictFetch a historical event from triggers.events, re-dispatch it through the trigger dispatcher, and thread replay_of_event_id through the returned DispatchHandle
trigger_inspect_dlq()listReturn the current DLQ snapshot as list<DlqEntry> with retry history and derived error_class
trigger_inspect_lifecycle(kind?)kind: string or nillistReturn trigger lifecycle event-log records, optionally filtered by event kind
trigger_inspect_action_graph(trace_id?)trace_id: string or nillistReturn streamed observability.action_graph records, optionally filtered to one trace id
trigger_test_harness(fixture)fixture: string or {fixture: string}dictRun a named trigger-system harness fixture and return a structured report. Intended for Rust/unit/conformance coverage of cron, webhook, retry, DLQ, dedupe, rate-limit, cost-guard, recovery, and dead-man-switch scenarios
handler_context()dict or nilReturn the active trigger dispatch context (agent, action, trace_id, replay_of_event_id, autonomy_tier, trigger_event) or nil outside dispatch
trust_record(agent, action, approver, outcome, tier)agent: string, action: string, approver: string or nil, outcome: string, tier: stringdictAppend a manual hash-chained TrustRecord to trust_graph and per-agent topics
trust_graph_record(decision)decision: dictstringAppend a hash-chained trust decision and return its TrustEntryId
trust_graph_query(agent, action)agent: string, action: string or nildictReturn a TrustScore summary and recommended capability policy for an agent/action pair
trust_graph_policy_for(agent)agent: stringdictReturn the capability policy derived from the agent's effective tier and trust history
trust_graph_verify_chain()nonedictVerify the active trust graph hash chain and return {verified, root_hash, errors, ...}
trust_query(filters)filters: dictlistQuery trust-graph records by agent, action, since, until, tier, outcome, limit, and/or grouped_by_trace
trust.query(filters)filters: dictlistQuery compact TrustGraphRecord rows by actor/actor_id/agent, action, outcome, since, until, autonomy_tier_at_time, and limit
trust.record(decision)decision: dictstringAppend a trust decision and return its TrustEntryId; accepts actor_id, action, approver, outcome, trace_id, autonomy_tier_at_time, evidence_refs, cost_usd, and metadata
trust.score(actor_id, action)actor_id: string, action: string or nildictReturn aggregate trust counters and the derived capability policy
trust.policy_for(actor_id)actor_id: stringdictReturn only the derived capability policy
trust.verify_chain()nonedictVerify the underlying OpenTrustGraph hash chain
llm_info()dictCurrent LLM config: {provider, model, api_key_set}
runtime_introspection()dictFull resolved runtime snapshot: {provider, model, model_alias, family, tool_format, tier, context_window, runtime_context_window, capabilities, harn_version, harness}. Fields stay nil until the first llm_call on the thread; harn_version and harness are always populated. See Runtime introspection tools for the model-callable tool surface (runtime_introspection_tools(reg)).
llm_usage()dictCumulative usage: {input_tokens, output_tokens, total_duration_ms, call_count, total_calls}
llm_resolve_model(alias)alias: stringdictResolve model alias or provider-prefixed selector to {id, provider, alias, tool_format, tier, family, lineage} via providers.toml
llm_model_info(model)model: stringdictReturn resolved model/provider metadata plus normalized family/lineage, catalog entry, capabilities, API-key availability, and QC default
llm_pick_model(target, options?)target: string, options: dictdictResolve a model alias or tier to {id, provider, tier}
llm_complementary_reviewer(options)options: {author_model, author_provider?, intent?, max_price_multiplier?}dictPick a different-family reviewer model for review, critique, or plan_review, returning the selected model, fallback reason when needed, and estimated incremental cost
llm_infer_provider(model_id)model_id: stringstringInfer provider from model ID (e.g. "claude-*""anthropic")
llm_model_tier(model_id)model_id: stringstringGet capability tier: "small", "mid", or "frontier"
llm_healthcheck(provider?, options?)provider: string or {provider, api_key?, model?}, options: {api_key?, model?} or model stringdictValidate a configured provider healthcheck. Returns {provider, valid, message, metadata}; api_key lets hosts validate a candidate key without first exporting it. For OpenAI-compatible /models healthchecks, passing a model (positional, {model: "..."}, or {provider, model: "..."}) verifies the selected model/alias is served and surfaces distinct metadata.category values such as unreachable, bad_status, model_missing, and invalid_url
llm_apply_reasoning_policy(opts)opts: dictdictApply Harn's provider-aware reasoning_policy / thinking_policy lowering to an llm_call option dict, preserving caller-supplied thinking or reasoning_effort
llm_rate_limit(provider, options?)provider: string, options: dictint/nil/bool/dictSet ({rpm: N, tpm: N, input_tpm: N, output_tpm: N, concurrency: N}), query legacy RPM, query rich details with {details: true}, or clear ({rpm: 0}) per-provider rate limits
llm_providers()listList all configured provider names
harness.llm.providers()listPer-provider availability + credential snapshot: [{name, available, credential_status}, ...]. credential_status is one of "ok", "missing", "not_required", "deferred"
llm_provider_status()listFree-builtin alias for harness.llm.providers(), available to scripts that do not receive a Harness parameter
llm_available_providers()listList providers usable in the current environment (auth configured or no auth required)
llm_known_models()listList configured model alias names
llm_qc_default_model(provider)provider: stringstring/nilReturn the configured cheap QC/repair model for a provider, honoring BURIN_QC_MODEL
llm_provider_catalog()dictReturn the loaded provider/model catalog: providers, aliases, model metadata, normalized family/lineage, pricing, QC defaults, and availability
llm_equivalent_models(selector)selector: stringlistReturn capability-compatible provider/model routes in the same logical-model equivalence group, excluding the source route
harness.llm.catalog()listReturn the full configured model catalog as a list of dicts: [{id, name, provider, context_window, runtime_context_window, capabilities, quality_tags, pricing, availability, deprecated, deprecation_note, ...}, ...]. Read-only view used by harn models list / harn models recommend
harness.llm.catalog_refresh(options?)options?: dict|nildictRefresh the process-wide provider/model catalog overlay from the configured hosted catalog, validating the remote document before installing it
llm_catalog()listFree-builtin alias for harness.llm.catalog(), available to scripts that do not receive a Harness parameter
llm_catalog_refresh(options?)options?: dict|nildictFree-builtin alias for harness.llm.catalog_refresh(options?), available to scripts that do not receive a Harness parameter
llm_config(provider?)provider: stringdictGet provider config (base_url, auth_style, etc.)
llm_cost(model, input_tokens, output_tokens)model: string, input_tokens: int, output_tokens: intfloatEstimate USD cost from catalog pricing, falling back to embedded pricing
llm_session_cost()dictSession totals: {total_cost, input_tokens, output_tokens, call_count}
llm_budget(max_cost)max_cost: floatnilSet session budget in USD. LLM calls pre-flight and throw if projected cost would exceed it
llm_budget_remaining()float or nilRemaining budget (nil if no budget set)
tiktoken_count_tokens(text, model)text: string, model: stringintCount text with the selected tiktoken encoder for known OpenAI models and labeled Claude/Gemini approximations
tiktoken_tokenizer_info(model)model: stringdictReturn {model, model_family, source, exact, known_model_family, encoder} for the encoder or heuristic fallback used by a model ID
llm_mock(response)response: dictnilQueue a mock LLM response. Dict supports text, tool_calls, blocks, logprobs, match (glob), consume_match (consume a matched pattern instead of reusing it), input_tokens, output_tokens, thinking, stop_reason, provider, model, error: {category, message?, retry_after_ms?} or provider envelopes error: {status, kind, reason?, message?, retry_after_ms?} (short-circuits the call and surfaces the same structured error dict as live provider failures — useful for testing llm_call_safe envelopes and retry loops)
llm_mock_calls()listReturn list of {messages, system, tools} for all calls made to the mock provider
llm_mock_clear()nilClear all queued mock responses and recorded calls

FIFO mocks (no match field) are consumed in order. Pattern-matched mocks (with match) are checked in declaration order against the request transcript text using glob patterns. They persist by default; add consume_match: true to advance through matching fixtures step by step. When no mocks match, the default deterministic mock behavior is used.

See Trigger stdlib for the typed std/triggers aliases, DLQ entry shapes, and the current shallow-path replay / manual-fire caveats.

Human in the loop

See Human in the loop for the full primitive catalog, event-log topics, bridge contract, and replay semantics.

FunctionParametersReturnsDescription
ask_user(prompt, options?)prompt: string, options: {schema?: Schema<T>, timeout?: duration, default?: T}TPause the current dispatch until the host supplies a response. Validates against schema when present, otherwise coerces toward default when possible. Defaults to a 24-hour timeout; on timeout, returns default or throws HumanTimeoutError
request_approval(action, options?)action: string, options: {detail?: any, args?: any, quorum?: int, reviewers?: list<string>, deadline?: duration, principal?: string, evidence_refs?: list<dict>, undo_metadata?: dict, capabilities_requested?: list<string>}{approved, reviewers, approved_at, reason, signatures}Emit a durable approval request, wait for quorum, and return the approval record with signed reviewer timestamp receipts. Defaults to quorum 1 and a 24-hour deadline. Denial throws ApprovalDeniedError
dual_control(n, m, action, approvers?)n: int, m: int, action: fn() -> T, approvers: list<string> or nilTn-of-m approval gate for executing action. Commonly used for destructive or privileged operations. Denial throws ApprovalDeniedError
escalate_to(role, reason)role: string, reason: string{request_id, role, reason, trace_id, status, accepted_at, reviewer}Raise the current dispatch to a higher-trust role and wait for host acceptance. The host or operator resolves it with harn.hitl.respond / harn orchestrator resume
hitl_pending(filters?)filters: {since?: string, until?: string, kinds?: list<string>, agent?: string, limit?: int} or nillist<{request_id, request_kind, agent, prompt, trace_id, timestamp, approvers, metadata}>Read the active event log's pending HITL requests as typed rows, newest first. Returns [] when no event log is attached.
// Queue specific responses for the mock provider
llm_mock({text: "The answer is 42."})
llm_mock({
  text: "Let me check that.",
  tool_calls: [{name: "read_file", arguments: {path: "main.rs"}}],
})
let r = llm_call("question", nil, {provider: "mock"})
assert_eq(r.text, "The answer is 42.")

// Pattern-matched mocks (reusable, not consumed)
llm_mock({text: "Hello!", match: "*greeting*"})
llm_mock({text: "step 1", match: "*planner*", consume_match: true})
llm_mock({text: "step 2", match: "*planner*", consume_match: true})

// Error injection for testing resilient code paths. The mock
// surfaces as a real `VmError::CategorizedError`, so `error_category`,
// `try { ... } catch`, `llm_call_safe`, and `with_rate_limit` all see
// it the same way they would a live provider failure.
llm_mock({error: {category: "rate_limit", message: "429 Too Many Requests"}})
llm_mock({error: {status: 503, kind: "transient", reason: "upstream_unavailable"}})

// Inspect what was sent
let calls = llm_mock_calls()
llm_mock_clear()

Transcript helpers

FunctionParametersReturnsDescription
transcript(metadata?)metadata: dicttranscriptCreate a new transcript
transcript_from_messages(messages_or_transcript)list or dicttranscriptNormalize a message list into a transcript
transcript_messages(transcript)transcript: dictlistGet transcript messages
transcript_summary(transcript)transcript: dictstring or nilGet transcript summary
transcript_id(transcript)transcript: dictstringGet transcript id
transcript_export(transcript)transcript: dictstringExport transcript as JSON
transcript_import(json_text)json_text: stringdictImport transcript JSON
transcript_fork(transcript, options?)transcript: dict, options: dicttranscriptFork transcript, optionally dropping messages or summary
transcript.inject_reminder(transcript, options)transcript: dict, options: dictdictReturn {transcript, reminder_id, deduped_count} after appending a pending system_reminder event. body is required; tags, dedupe_key, ttl_turns, preserve_on_compact, propagate, and role_hint are optional and validated. A matching dedupe_key replaces older pending reminders and emits a lifecycle event when EventLog is active.
transcript.clear_reminders(transcript, selector)transcript: dict, selector: dictdictReturn {transcript, removed_count} after removing pending reminders selected by id, tag, or dedupe_key; multiple selectors are combined with AND semantics.
transcript_summarize(transcript, options?)transcript: dict, options: dicttranscriptSummarize and compact a transcript via llm_call
transcript_compact(transcript, options?)transcript: dict, options: dicttranscriptCompact a transcript with the runtime compaction engine, preserving durable artifacts and compaction events. Pending reminders are TTL-processed and deduped before compaction; only preserve_on_compact: true reminders survive verbatim. strategy: "custom" requires custom_compactor, a closure called with (messages, reminders) that returns the replacement transcript state
transcript_auto_compact(messages, options?)messages: list, options: dictlistApply the agent-loop compaction pipeline to a message list using llm, truncate, or custom strategy

Provider configuration

LLM provider endpoints, model aliases, inference rules, and default parameters are configured via a TOML file. The VM searches for config in this order:

  1. Built-in defaults (Anthropic, OpenAI, OpenRouter, HuggingFace, Ollama, Local, llama.cpp)
  2. HARN_PROVIDERS_CONFIG if set, otherwise ~/.config/harn/providers.toml
  3. Installed package [llm] tables in .harn/packages/*/harn.toml
  4. The nearest project harn.toml [llm] table

The files in steps 2-4 are overlays on the built-in defaults. The [llm] section uses the same schema as providers.toml, so project and package manifests can ship provider adapters declaratively:

[llm.providers.anthropic]
base_url = "https://api.anthropic.com/v1"
auth_style = "header"
auth_header = "x-api-key"
auth_env = "ANTHROPIC_API_KEY"
chat_endpoint = "/messages"

[llm.providers.local]
base_url = "http://localhost:8000"
base_url_env = "LOCAL_LLM_BASE_URL"
auth_style = "none"
chat_endpoint = "/v1/chat/completions"
completion_endpoint = "/v1/completions"

[llm.providers.llamacpp]
base_url = "http://127.0.0.1:8001"
base_url_env = "LLAMACPP_BASE_URL"
auth_style = "none"
chat_endpoint = "/v1/chat/completions"
completion_endpoint = "/v1/completions"

[llm.aliases]
sonnet = { id = "claude-sonnet-4-6", provider = "anthropic" }

[[llm.inference_rules]]
pattern = "claude-*"
provider = "anthropic"

[[llm.tier_rules]]
pattern = "claude-*"
tier = "frontier"

[llm.model_defaults."qwen/*"]
temperature = 0.3

[llm.model_roles.merge]
provider = "ollama"
model = "devstral-small-2"
temperature = 0.0

Timers

FunctionParametersReturnsDescription
timer_start(name?)name: stringdictStart a named timer
timer_end(timer)timer: dictintStop timer, prints elapsed, returns milliseconds
elapsed()intMilliseconds since process start

Circuit breakers

Protect against cascading failures by tracking error counts and opening a circuit when a threshold is reached.

FunctionParametersReturnsDescription
circuit_breaker(name, threshold?, reset_ms?)name: string, threshold: int (default 5), reset_ms: int (default 30000)stringCreate a named circuit breaker. Returns the name
circuit_check(name)name: stringstringCheck state: "closed", "open", or "half_open" (after reset period)
circuit_record_failure(name)name: stringboolRecord a failure. Returns true if the circuit just opened
circuit_record_success(name)name: stringnilRecord a success, resetting failure count and closing the circuit
circuit_reset(name)name: stringnilManually reset the circuit to closed

Example:

circuit_breaker("api", 3, 10000)

for i in 0 to 5 exclusive {
  if circuit_check("api") == "open" {
    log("circuit open, skipping call")
  } else {
    try {
      let resp = http_get("https://api.example.com/data")
      circuit_record_success("api")
    } catch e {
      circuit_record_failure("api")
    }
  }
}

Runtime context

Logical task, workflow, trigger, agent-session, and trace introspection. Use this instead of raw OS thread identity.

FunctionParametersReturnsDescription
runtime_context()nonedictReturn the current logical runtime context with task, workflow, trigger, agent, trace, cancellation, debug, and task-local value fields
task_current()nonedictAlias for runtime_context()
runtime_context_values()nonedictReturn task-local context values for the current logical task
runtime_context_get(key, default?)key: string, default: anyanyReturn a task-local value, the provided default, or nil
runtime_context_set(key, value)key: string, value: anyanySet a task-local value and return the previous value or nil
runtime_context_clear(key)key: stringanyClear a task-local value and return the previous value or nil

Children created by spawn, parallel, parallel each, and parallel settle inherit a snapshot of task-local values. Child writes do not mutate the parent context.

Tracing

Distributed tracing primitives for instrumenting pipeline execution.

FunctionParametersReturnsDescription
trace_start(name)name: stringdictStart a trace span. Returns a span dict with trace_id, span_id, name, start_ms
trace_end(span)span: dictnilEnd a span and emit a structured log line with duration
trace_id()nonestring or nilCurrent trace ID from the span stack, or nil if no active span
enable_tracing(enabled?)enabled: bool (default true)nilEnable or disable pipeline-level tracing
trace_spans()nonelistPeek at recorded trace spans
trace_summary()nonestringFormatted summary of trace spans

Example:

let span = trace_start("fetch_data")
// ... do work ...
trace_end(span)

log(trace_summary())

Agent trace events

Fine-grained agent loop trace events for observability and debugging. Events are collected during agent_loop execution and can be inspected after the loop completes.

FunctionParametersReturnsDescription
agent_trace()nonelistPeek at collected agent trace events. Each event is a dict with a type field (llm_call, tool_execution, tool_rejected, loop_intervention, context_compaction, phase_change, loop_complete) and type-specific fields
agent_trace_summary()nonedictRolled-up summary of agent trace events with aggregated token counts, durations, tool usage, and iteration counts

Example:

let result = agent_loop("summarize this file", tools: [read_file])
let summary = agent_trace_summary()
log("LLM calls: " + str(summary.llm_calls))
log("Tools used: " + str(summary.tools_used))

Error classification

Structured error throwing and classification for retry logic and error handling.

FunctionParametersReturnsDescription
throw_error(message, category?)message: string, category: stringneverThrow a categorized error. The error is a dict with message and category fields
error_category(err)err: anystringExtract category from a caught error. Returns "timeout", "auth", "rate_limit", "tool_error", "tool_rejected", "egress_blocked", "cancelled", "not_found", "circuit_open", or "generic"
is_timeout(err)err: anyboolCheck if error is a timeout
is_rate_limited(err)err: anyboolCheck if error is a rate limit

Example:

try {
  throw_error("request timed out", "timeout")
} catch e {
  if is_timeout(e) {
    log("will retry after backoff")
  }
  log(error_category(e))  // "timeout"
}

Tool registry (low-level)

Low-level tool management functions for building and inspecting tool registries programmatically. For MCP serving, see the tool_define / mcp_tools API above.

For declarative batches, import { tool_define_many, tool_registry_from } from "std/tools". tool_define_many(registry, specs: list<ToolDefinitionSpec>) adds typed {name, description, parameters, handler, ...} specs to a ToolRegistry, and tool_registry_from(specs) creates a fresh registry from the same shape.

FunctionParametersReturnsDescription
tool_remove(registry, name)registry, name: stringdictRemove a tool by name
tool_list(registry)registry: dictlistList tools as [{name, description, parameters}]
tool_find(registry, name)registry, name: stringdict or nilFind a tool entry by name
tool_select(registry, names)registry: dict, names: listdictReturn a registry containing only the named tools
tool_count(registry)registry: dictintNumber of tools in the registry
tool_describe(registry)registry: dictstringHuman-readable summary of all tools
tool_schema(registry, components?)registry, components: dictdictGenerate JSON Schema for all tools
tool_prompt(registry)registry: dictstringGenerate an LLM system prompt describing available tools
tool_parse_call(text)text: stringlistParse <tool_call>...</tool_call> XML from LLM output
tool_format_result(name, result)name, result: stringstringFormat a <tool_result> XML envelope

composition_binding_manifest(tool_list(registry)) projects a registry into a prompt-visible Code Mode API. The default manifest hides denied tools; pass {include_denied: true} only when producing audit/debug examples.

Structured logging

FunctionParametersReturnsDescription
log_json(key, value)key: string, value: anynilEmit a JSON log line with timestamp

Metadata

Project metadata store backed by host-managed sharded JSON files. Supports hierarchical namespace resolution (child directories inherit from parents). The default filesystem backend persists namespace shards under .harn/metadata/<namespace>/entries.json and still reads the legacy monolithic root.json shard.

Directory entries inherit; file entries do not. The shard schema is shared: each namespace shard has a directory map under entries and an optional file map under files, both keyed by normalized relative path. Shards without a files section load unchanged.

FunctionParametersReturnsDescription
metadata_get(dir, namespace?)dir: string, namespace: stringdict | nilRead metadata with inheritance
metadata_resolve(dir, namespace?)dir: string, namespace: stringdict | nilRead resolved metadata while preserving namespaces
metadata_entries(namespace?)namespace: stringlistList stored directories with local and resolved metadata
metadata_set(dir, namespace, data)dir: string, namespace: string, data: dictnilWrite metadata for directory/namespace
metadata_save()nilFlush metadata to disk
metadata_stale(project)project: stringdictCheck staleness: {any_stale, tier1, tier2}
metadata_status(namespace?)namespace: stringdictSummarize directory counts, namespaces, missing hashes, and stale state
metadata_refresh_hashes()nilRecompute content hashes
compute_content_hash(dir)dir: stringstringHash of directory contents
invalidate_facts(dir)dir: stringnilMark cached facts as stale
path_metadata_get(path, namespace?, opts?)path: string, namespace: string, opts: {kind?: "file"|"dir"}dict | nilRead metadata at an exact path. File entries (default) do not inherit; {kind: "dir"} falls back to hierarchical resolution.
path_metadata_set(path, namespace, data, opts?)path: string, namespace: string, data: dict, opts: {kind?: "file"|"dir"}nilWrite metadata at an exact path. Defaults to {kind: "file"}.
path_metadata_entries(namespace?, opts?)namespace: string, opts: {kind?: "file"|"dir"|"all"}listList stored entries keyed by normalized relative path. Defaults to files only.
scan_directory(path?, pattern_or_options?, options?)path: string, pattern: string or options: dictlistEnumerate files and directories with optional pattern, max_depth, include_hidden, include_dirs, include_files

Project introspection

FunctionParametersReturnsDescription
project_fingerprint(path?)path: stringProjectFingerprintReturn a normalized shallow project profile for the current working directory or the supplied path
project_context_profile(path?, options?)path: string, options: {signals?: dict, fingerprint?: dict, remote?: string | dict, credentials?: list | dict, include_env_credentials?: bool}ProjectContextProfileResolve project signals into active profile IDs, prompt fragments, skills, tool groups, MCP preset candidates, caps, redacted signal provenance, and token-delta metadata

ProjectFingerprint has these fields:

  • primary_language: "rust", "typescript", "python", "go", "swift", "ruby", "mixed", or "unknown"
  • languages: all detected top-level languages in stable order
  • frameworks: shallow framework signals such as "axum", "next", "react", "django", "fastapi", or "rails"
  • package_manager: the dominant normalized package manager tag such as "cargo", "spm", "pnpm", "npm", "uv", "poetry", "pip", "go-mod", or "bundler"
  • package_managers: detected package managers such as "cargo", "npm", "pnpm", "yarn", "uv", "poetry", "pip", "go-mod", or "bundler"
  • test_runner: the dominant normalized test runner tag such as "nextest", "cargo-test", "vitest", "pytest", "go-test", or "xctest"
  • build_tool: the dominant normalized build tool tag such as "cargo", "spm", "next", "vite", "uv", "poetry", or "go"
  • vcs: "git", "hg", or nil when no VCS root is detected
  • ci: detected CI providers such as "github-actions", "gitlab-ci", "circleci", "buildkite", "azure-pipelines", or "bitrise"
  • has_tests: true when a standard test directory such as tests/, test/, __tests__/, or spec/ is present
  • has_ci: true when CI config such as .github/workflows/ or .gitlab-ci.yml is present
  • lockfile_paths: relative paths to detected lockfiles such as Cargo.lock, package-lock.json, pnpm-lock.yaml, uv.lock, go.sum, or Package.resolved

ProjectContextProfile has these top-level fields:

  • profile_ids: active profile IDs such as "git", "github", "rust", "node", "python", and "swift"
  • prompt_fragments: fragments accepted by the prompt reducer and gated by requires_caps
  • skills, tool_groups, mcp_presets, and mcp_preset_candidates: activation metadata for existing skill/tool/preset surfaces
  • caps: capability flags that explain profile-fragment inclusion in prompt_explain(...)
  • signals: normalized project fingerprint, redacted Git remote, signal source, and credential aliases
  • token_delta: estimated tokens/bytes for activated profile fragments versus the always-on profile catalog

Secret scanning

FunctionParametersReturnsDescription
secret_scan(content)content: stringlistScan text or diffs for high-signal leaked credentials and return redacted findings with detector metadata and source locations
self_review(diff, rubric?, max_rounds?)diff: string, rubric: string, max_rounds: intdictRun a structured pre-PR self-review over a diff, merge in secret_scan blockers, and append a pr.self_review trust-graph record with review metadata

self_review(...) uses the existing tier-based model resolver with model_tier: "small" today, so it benefits from Harn's current provider/model fallback chain without waiting on the broader routing DSL work.

The builtin accepts either a custom rubric string or one of the built-in preset names:

  • default — correctness, test coverage, security, and style
  • code — correctness, regressions, tests, and API compatibility
  • docs — accuracy, implementation drift, examples, and migration notes
  • infra — rollout safety, observability, failure modes, and rollback posture
  • security — credential exposure, auth, data handling, and hardening gaps

It returns a structured result with:

  • summary
  • findings
  • has_blocking_findings
  • rounds
  • secret_scan_findings
  • trust_record

MCP (Model Context Protocol)

Connect to external tool servers using the Model Context Protocol. Harn supports stdio transport (spawns a child process) and HTTP transport for remote MCP servers.

FunctionParametersReturnsDescription
mcp_roots() / harn.mcp.roots()nonelistReturn the MCP roots Harn exposes to connected servers (uri, name, path)
mcp_configure(config) / harn.mcp.configure(config)config: dictdictOpt into experimental MCP behavior for the current VM, including draft SEP-2356 file inputs
mcp_file_input(options?) / harn.mcp.file_input(options?)options: dictdictReturn a JSON Schema property using the draft x-mcp-file annotation
mcp_upload_file(server, file_path, options?) / harn.mcp.upload_file(server, file_path, options?)server: mcp_client, file_path: string, options: dictstringEncode a local file as an RFC 2397 data: URI for an experimental MCP file input
mcp_connect(command, args?, options?)command: string, args: list, options: dictmcp_clientSpawn an MCP server and connect with the legacy or opt-in RC client profile
mcp_list_tools(client)client: mcp_clientlistList available tools from the server
mcp_call(client, name, arguments?)client: mcp_client, name: string, arguments: dictstring or listCall a tool and return the result
mcp_list_resources(client)client: mcp_clientlistList available resources from the server
mcp_list_resource_templates(client)client: mcp_clientlistList resource templates (URI templates) from the server
mcp_read_resource(client, uri)client: mcp_client, uri: stringstring or listRead a resource by URI
mcp_list_prompts(client)client: mcp_clientlistList available prompts from the server
mcp_get_prompt(client, name, arguments?)client: mcp_client, name: string, arguments: dictdictGet a prompt with optional arguments
mcp_server_info(client)client: mcp_clientdictGet connection info (name, connected) plus the server initialize response and extracted advisory instructions when supplied
mcp_disconnect(client)client: mcp_clientnilKill the server process and release resources

Example:

let client = mcp_connect("npx", ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"])
let tools = mcp_list_tools(client)
log(tools)

let result = mcp_call(client, "read_file", {"path": "/tmp/hello.txt"})
log(result)

mcp_disconnect(client)

Notes:

  • MCP file inputs are experimental and default off. Harn implements the current draft SEP-2356 shape: x-mcp-file on uri string schema properties and inline RFC 2397 data: URI values. Enable it explicitly with harn.mcp.configure({experimental: {file_upload: {spec_revision: "modelcontextprotocol/modelcontextprotocol#2356"}}}).
  • mcp_call returns a string when the tool produces a single text block, a list of content dicts for multi-block results, or nil when empty.
  • HTTP MCP clients keep the server's Streamable HTTP GET event stream open after legacy initialization. RC clients use stateless request/response HTTP with per-request metadata instead.
  • Set options.protocol_mode to "rc" for direct stdio connects, or protocol_mode = "rc" in harn.toml, to opt into the draft MCP client profile. Harn probes RC stdio servers with server/discover, attaches MCP version/client/capability metadata to every request, retries a mutually supported version on unsupported-version errors, and falls back to the legacy initialize handshake only when server/discover is not implemented.
  • In RC HTTP mode Harn sends MCP-Protocol-Version, Mcp-Method, and Mcp-Name where required, does not require MCP-Session-Id, mirrors x-mcp-header tool-schema annotations into Mcp-Param-* headers for tools/call, and handles input_required tool results by resolving roots, elicitation, and sampling requests before retrying the call.
  • If the tool reports isError: true, mcp_call throws the error text.
  • mcp_connect throws if the command cannot be spawned or the initialize handshake or RC discovery probe fails.

Auto-connecting MCP servers via harn.toml

Instead of calling mcp_connect manually, you can declare MCP servers in harn.toml. They will be connected automatically before the pipeline executes and made available through the global mcp dict.

Add a [[mcp]] entry for each server:

[[mcp]]
name = "filesystem"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"]

[[mcp]]
name = "github"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-github"]

Each entry requires:

FieldTypeDescription
namestringIdentifier used to access the client (e.g., mcp.filesystem)
commandstringExecutable to spawn for stdio transports
argslist of stringsCommand-line arguments for stdio transports (default: empty)
transportstringstdio (default) or http
urlstringRemote MCP server URL for HTTP transports
auth_tokenstringOptional explicit bearer token for HTTP transports
client_idstringOptional pre-registered OAuth client ID for HTTP transports
client_secretstringOptional pre-registered OAuth client secret
scopesstringOptional OAuth scope string for login/consent
protocol_versionstringOptional MCP protocol version override
protocol_modestringOptional MCP client profile: legacy (default) or rc

The connected clients are available as properties on the mcp global dict:

pipeline default() {
  let tools = mcp_list_tools(mcp.filesystem)
  log(tools)

  let result = mcp_call(mcp.github, "list_issues", {repo: "harn"})
  log(result)
}

If a server fails to connect, a warning is printed to stderr and that server is omitted from the mcp dict. Other servers still connect normally. The mcp global is only defined when at least one server connects successfully.

For HTTP MCP servers, use the CLI to establish OAuth once and let Harn reuse the stored token automatically:

harn mcp redirect-uri
harn mcp login notion

MCP server mode

Harn pipelines can expose tools, resources, resource templates, and prompts as an MCP server using harn serve mcp. The CLI serves them over stdio or Streamable HTTP using the MCP protocol, making them callable by Claude Desktop, Cursor, or any MCP client.

Declarative syntax (preferred):

tool greet(name: string) -> string {
  description "Greet someone by name"
  "Hello, " + name + "!"
}

The tool keyword declares a tool with typed parameters, an optional description, and a body. Parameter types map to JSON Schema (string -> "string", int -> "integer", float -> "number", bool -> "boolean"). Parameters with default values are emitted as optional schema fields (required: false) and carry their default value into the generated tool registry entry. Each tool declaration produces its own tool registry dict.

Programmatic API:

FunctionParametersReturnsDescription
tool_registry()tool registryCreate an empty {_type: "tool_registry", tools: []} registry
tool_define(registry, name, desc, config)registry, name, desc: string, config: dictdictAdd a tool (config: {parameters, handler, returns?, annotations?, ...})
tool_define_many(registry, specs)registry: ToolRegistry, specs: list<ToolDefinitionSpec>ToolRegistryStdlib helper from std/tools; add many declarative tool specs to a registry
tool_registry_from(specs)specs: list<ToolDefinitionSpec>ToolRegistryStdlib helper from std/tools; create a registry from declarative tool specs
tool_synthesize(config)config: dictclosureSynthesize a deterministic callable tool from a natural-language description
tool_synthesis_cache()listInspect pinned synthesized tool specs for the current run
tool_synthesis_clear()nilClear the current run's synthesized tool cache
composition_binding_manifest(tools, options?)tools: list or dict, options?: dictdictBuild a stable Code Mode binding manifest from Harn, host bridge, MCP, provider-native, or deferred tools
composition_execute(snippet, manifest, options?)snippet: string, manifest: dict, options?: dictdictExecute a read-only Harn composition snippet and return parent/child audit data
composition_search_examples(query?, limit?)query?: string, limit?: intlistReturn curated read-only composition examples
composition_harn_api(manifest)manifest: dictstringEmit typed Harn wrapper declarations for a Code Mode binding manifest
composition_typescript_declarations(manifest)manifest: dictstringEmit declaration-only TypeScript bindings from the manifest
composition_crystallization_trace(report, options?)report: dict, options?: dictdictConvert a composition report into crystallization trace input
mcp_tools(registry)registry: dictnilRegister tools for MCP serving
mcp_resource(config)config: dictnilRegister a static resource ({uri, name, text, description?, mime_type?})
mcp_resource_template(config)config: dictnilRegister a resource template ({uri_template, name, handler, description?, mime_type?, completions?}); completions maps URI variable names to static suggestion lists or completion closures
mcp_prompt(config)config: dictnilRegister a prompt ({name, handler, description?, arguments?}); prompt arguments may include suggestions/completions or a complete closure for MCP completion/complete
mcp_report_progress(progress, opts?)progress: number, opts?: dictboolEmit a notifications/progress update for the in-flight MCP tool call (no-op when the client did not opt in via _meta.progressToken). opts: {total?: number, message?: string, token?: string|number}

The composition_* builtins back Governed Code Mode. The executor is read-only: it rejects imports, writes, process execution, network access, direct HITL, parallel/spawn, and calls outside the manifest bindings and pure data helpers. Composition snippets may use map_bounded(...) for settled fan-out; child binding calls still honor global and per-MCP-server concurrency caps, retry policy, trusted MCP annotation gates, idempotency keys, and outputSchema validation. std/composition adds MCP-focused helpers including composition_mcp_api(...), composition_mcp_execute(...), and composition_mcp_tools(...), which registers the compact MCP profile tools harn.code.search_examples, harn.code.generate_harn_api, and harn.code.execute_composition. The MCP executor profile returns only a reduced result envelope by default; use composition_execute(...) directly for the full child audit report.

tool_synthesize(config) is the guarded natural-language tool bootstrapper. It returns a callable closure and pins the synthesis in an in-memory cache keyed by a deterministic hash of the normalized description, schemas, capabilities, and executor. By default the synthesized tool is executor: "dry_run": calling it validates required arguments and returns a structured preview, but does not hit external systems.

let calendar = tool_synthesize({
  description: "fetch the user's calendar for a given date range",
  name: "calendar_fetch",
  parameters: {
    start: {type: "string"},
    end: {type: "string"},
  },
  return_type: {type: "object", properties: {events: {type: "array"}}},
  capabilities: ["http", "oauth:google"],
})

let preview = calendar({start: "2026-04-29", end: "2026-05-06"})

To dispatch for real, the synthesis must declare an explicit backend: executor: "host_bridge" with host_tool, or executor: "mcp_server" with mcp_tool and a connected mcp_client. Host and MCP calls still pass through the active execution policy. tool_synthesize does not accept arbitrary generated Harn handlers; use tool_define when the executable body is known and reviewed.

Tool annotations (MCP spec annotations field) can be passed in the tool_define config to describe tool behavior:

tools = tool_define(tools, "search", "Search files", {
  parameters: { query: {type: "string"} },
  returns: {type: "string"},
  handler: { args -> "results for ${args.query}" },
  annotations: {
    title: "File Search",
    readOnlyHint: true,
    destructiveHint: false
  }
})

Unknown tool_define config keys are preserved on the tool entry. Workflow graphs use this to carry runtime policy metadata directly on a tool registry, for example:

tools = tool_define(tools, "read", "Read files", {
  parameters: { path: {type: "string"} },
  returns: {type: "string"},
  handler: nil,
  policy: {
    capabilities: {workspace: ["read_text"]},
    side_effect_level: "read_only",
    path_params: ["path"],
    mutation_classification: "read_only"
  }
})

When a workflow node uses that registry, Harn intersects the declared tool policy with the graph, node, and host ceilings during validation and at execution time.

Declarative tool approval

agent_loop, workflow_execute, and workflow stage nodes accept an approval_policy option that declaratively gates tool calls:

agent_loop("task", "system", {
  approval_policy: {
    rules: [
      {deny: {path: "**/.env*"}, reason: "credential file"},
      {ask: {tool: "edit_*", path: "src/**"}, reason: "workspace edit"},
      {deny: {tool: "run_command", command: ["*curl*", "*wget*"]}}
    ],
    auto_approve: ["read*", "list_*"],
    auto_deny: ["shell*"],
    require_approval: ["edit_*", "write_*"],
    write_path_allowlist: ["/workspace/**"],
    repeat_limit: 3
  }
})

rules is the typed policy DSL. Each rule uses allow, ask, or deny shorthand, or {action: "ask", match: {...}}. Match fields are ANDed inside a rule and accept strings or string lists:

FieldMatches
toolTool-name glob
tool_kindAnnotated ACP tool kind (read, edit, execute, fetch, ...)
side_effectAnnotated side-effect level (read_only, workspace_write, process_exec, network)
pathDeclared path arguments from ToolAnnotations.arg_schema.path_params
command / command_identityShell text or normalized argv/program identity
url / domain / methodURL strings, normalized host domains, and HTTP methods found in tool args
mcp_server / mcp_toolMCP owner inferred from <server>__<tool> names or MCP arg metadata
agent / persona / modeAgent/persona/mode identity from args or trigger context
capabilityAnnotated capability operation such as workspace.read_text
repeat_count_gteSame (session, tool, args) call count threshold

Deny beats ask, and ask beats allow regardless of rule order. Legacy auto_deny, require_approval, and auto_approve are evaluated through the same precedence model, so old policy bags remain valid while the richer DSL is available. Tools that match no pattern default to AutoApproved.

When an approval_policy is active, Harn denies sensitive path strings such as .env, private keys, and credential files by default. Declared host-absolute paths outside the workspace are also denied unless external_roots explicitly allows the root or allow_external_paths: true is set. Set allow_sensitive_paths: true only when a host already mediates secret reads.

ask and require_approval call the host via the canonical ACP session/request_permission request and fail closed if the host does not implement it. The prompt payload includes a policyDecision receipt with the matched rule, risk labels, normalized context, and rationale. Permission grant and denial transcript events carry the same receipt under metadata.policy_decision for replay and audit.

Example profiles:

let local_dev_policy = {
  rules: [
    {allow: {tool_kind: ["read", "search"]}},
    {ask: {tool_kind: ["edit", "move"], path: "src/**"}, reason: "workspace mutation"},
    {deny: {command: ["*curl*", "*wget*"]}, reason: "downloaded shell is not allowed"}
  ],
  repeat_limit: 3
}

let ci_headless_policy = {
  rules: [
    {allow: {tool_kind: ["read", "search"]}},
    {allow: {tool: "run_command", command_identity: ["cargo", "npm", "make"]}},
    {deny: "*"}
  ],
  allow_sensitive_paths: false,
  repeat_limit: 1,
  repeat_action: "deny"
}

let managed_enterprise_policy = {
  rules: [
    {ask: {side_effect: ["workspace_write", "process_exec", "network"]}, reason: "managed approval"},
    {deny: {domain: ["*.pastebin.com", "*.ngrok.io"]}},
    {deny: {path: ["**/.env*", "**/.aws/credentials"]}}
  ],
  external_roots: ["/tmp/harn-approved"]
}

Policies compose across nested scopes with most-restrictive intersection: auto-deny and require-approval take the union, while auto_approve and write_path_allowlist take the intersection. Rule lists concatenate and retain deny/ask/allow precedence; repeat limits keep the smaller threshold.

Example (agent.harn):

pipeline main(task) {
  var tools = tool_registry()
  tools = tool_define(tools, "greet", "Greet someone", {
    parameters: { name: {type: "string"} },
    returns: {type: "string"},
    handler: { args -> "Hello, ${args.name}!" }
  })
  mcp_tools(tools)

  mcp_resource({
    uri: "docs://readme",
    name: "README",
    text: "# My Agent\nA demo MCP server."
  })

  mcp_resource_template({
    uri_template: "config://{key}",
    name: "Config Values",
    handler: { args -> "value for ${args.key}" }
  })

  mcp_prompt({
    name: "review",
    description: "Code review prompt",
    arguments: [{ name: "code", required: true }],
    handler: { args -> "Please review:\n${args.code}" }
  })
}

Run as an MCP server:

harn serve mcp agent.harn

Configure in Claude Desktop (claude_desktop_config.json):

{
  "mcpServers": {
    "my-agent": {
      "command": "harn",
      "args": ["serve", "mcp", "agent.harn"]
    }
  }
}

Notes:

  • mcp_tools(registry) (or the alias mcp_serve) must be called to register tools.
  • Resources, resource templates, and prompts are registered individually.
  • All print/println output goes to stderr (stdout is the MCP transport in stdio mode).
  • The server supports the 2025-11-25 MCP protocol version over stdio and Streamable HTTP.
  • Tool handlers receive arguments as a dict and should return a string result.
  • Prompt handlers receive arguments as a dict and return a string (single user message) or a list of {role, content} dicts.
  • Resource template handlers receive URI template variables as a dict and return the resource text.

Workflow and orchestration builtins

These builtins expose Harn's typed orchestration runtime.

Workflow graph and planning

FunctionParametersReturnsDescription
workflow_graph(config)config: dictworkflow graphNormalize a workflow definition into the typed workflow IR
workflow_validate(graph, ceiling?)graph: workflow, ceiling: dict (optional)dictValidate graph structure and capability ceilings
workflow_inspect(graph, ceiling?)graph: workflow, ceiling: dict (optional)dictReturn graph plus validation summary
workflow_clone(graph)graph: workflowworkflow graphClone a workflow and append an audit entry
workflow_insert_node(graph, node, edge?)graph, node, edgeworkflow graphInsert a node and optional edge
workflow_replace_node(graph, node_id, node)graph, node_id, nodeworkflow graphReplace a node definition
workflow_rewire(graph, from, to, branch?)graph, from, to, branchworkflow graphRewire an edge
workflow_set_model_policy(graph, node_id, policy)graph, node_id, policyworkflow graphSet per-node model policy
workflow_set_context_policy(graph, node_id, policy)graph, node_id, policyworkflow graphSet per-node context policy
workflow_set_auto_compact(graph, node_id, policy)graph, node_id, policyworkflow graphSet per-node auto-compaction policy
workflow_set_output_visibility(graph, node_id, visibility)graph, node_id, visibilityworkflow graphSet per-node output-visibility filter ("public"/"public_only"/nil)
workflow_policy_report(graph, ceiling?)graph, ceiling: dict (optional)dictInspect workflow/node policies against an explicit or builtin ceiling
workflow_diff(left, right)left, rightdictCompare two workflow graphs
workflow_commit(graph, reason?)graph, reasonworkflow graphValidate and append a commit audit entry

Workflow execution and run records

FunctionParametersReturnsDescription
workflow_execute(task, graph, artifacts?, options?)task, graph, artifacts, optionsdictExecute a workflow and persist a run record
run_record(payload)payload: dictrun recordNormalize a run record
run_record_save(run, path?)run, pathdictPersist a run record
run_record_load(path)path: stringrun recordLoad a run record from disk
load_run_tree(path)path: stringdictLoad a persisted run with delegated child-run lineage
run_record_fixture(run)runreplay fixtureDerive a replay/eval fixture from a saved run
run_record_eval(run, fixture?)run, fixturedictEvaluate a run against an embedded or explicit fixture
run_record_eval_suite(cases)cases: listdictEvaluate a list of {run, fixture?, path?} cases as a regression suite
run_record_diff(left, right)left, rightdictCompare two run records and summarize stage/status deltas
eval_pack_manifest(payload)payload: dictdictNormalize an eval pack manifest
eval_pack_validate_split(manifest)manifest: dictdictValidate an eval pack split declaration
eval_pack_run(manifest, options?)manifest: dict, options: dictdictEvaluate replay or live-verify eval pack cases with trial-level ledger resume
eval_ledger_read(options?)options: dictdictRead durable eval-ledger rows
eval_ledger_append_rows(rows, options?)rows: list or dict, options: dictdictAppend idempotent eval-ledger rows
eval_ledger_append_unique_case_rows(rows, options?)rows: list or dict, options: dictdictAppend eval-ledger rows keyed by case/trial fingerprints
eval_ledger_prior_commit_rows(options)options: dictdictRead latest prior-commit eval rows and fingerprint mismatches
eval_ledger_resolve_resume_plan(manifest, options?)manifest: dict, options: dictdictResolve which eval-pack trial cells will run or be skipped
skill_induce(payload)payload: dictdictInduce replay-gated SKILL.md candidates from crystallization traces
persona_eval_ladder_manifest(payload)payload: dictdictNormalize a persona eval timeout/budget ladder manifest
persona_eval_ladder_run(manifest)manifest: dictdictRun a persona eval ladder and write per-tier transcript, receipt, and summary artifacts
eval_suite_manifest(payload)payload: dictdictNormalize a grouped eval suite manifest
eval_suite_run(manifest)manifest: dictdictEvaluate a manifest of saved runs, fixtures, and optional baselines
friction_event(payload)payload: dictdictNormalize a redacted friction event for repeated queries, clarifications, approval stalls, missing context, handoffs, tool gaps, failed assumptions, expensive deterministic steps, or human hypotheses
friction_record(payload, options?)payload: dict, options: dictdictRecord a friction event to the process-local buffer, append JSONL with log_path/HARN_FRICTION_LOG, or no-op when enabled: false
friction_events()listReturn process-local friction events recorded in the current VM
friction_clear()nilClear process-local friction events
context_pack_manifest(payload)payload: dictdictValidate and normalize a context-pack manifest
context_pack_manifest_parse(src)src: TOML or JSON stringdictParse and validate a context-pack manifest
context_pack_suggestions(events?, options?)events: list or {events}, options: dictlistGenerate candidate context-pack/workflow suggestions from repeated friction evidence
friction_eval_fixture(fixture)fixture: {events, options?, expected_suggestions?}dictEvaluate a repeated-friction fixture and assert expected context-pack suggestions
eval_metric(name, value, metadata?)name: string, value: any, metadata: dictnilRecord a named metric into the eval metric store
eval_metrics()listReturn all recorded eval metrics as {name, value, metadata?} dicts

workflow_execute options currently include:

  • max_steps
  • persist_path
  • resume_path
  • resume_run
  • replay_path
  • replay_run
  • replay_mode ("deterministic" currently replays saved stage fixtures)
  • parent_run_id
  • root_run_id
  • execution ({cwd?, env?, worktree?} for isolated delegated execution)
  • audit (seed mutation-session metadata for trust/audit grouping)
  • mutation_scope
  • approval_policy (declarative tool approval policy; see below)

verify nodes may also define execution checks inside node.verify, including:

  • command to execute via the host shell in the current execution context
  • assert_text to require visible output to contain a substring
  • expect_status to require a specific exit status

Workflow messaging and lifecycle

FunctionParametersReturnsDescription
workflow.signal(target, name, payload?)target, name: string, payload: anydictEnqueue a fire-and-forget signal message for a workflow
workflow.query(target, name)target, name: stringanyRead the last published query value, or nil when absent
workflow.publish_query(target, name, value?)target, name: string, value: anydictPublish or replace a named query value for a workflow
workflow.update(target, name, payload?, options?)target, name: string, payload: any, options: dictanyEnqueue an update request and wait for a matching response
workflow.receive(target)targetdict or nilPop the next queued message (signal, update, or control message)
workflow.respond_update(target, request_id, value, name?)target, request_id: string, value: any, name: string (optional)dictFulfill a pending workflow update request
workflow.pause(target)targetdictMark a workflow paused and enqueue a control message
workflow.resume(target)targetdictMark a workflow resumed and enqueue a control message
workflow.status(target)targetdictReturn mailbox/generation status for a workflow
workflow.continue_as_new(target)targetdictAdvance the workflow generation and clear pending update responses
continue_as_new(target)targetdictTop-level alias for workflow.continue_as_new(...)

target may be either a workflow-id string or a dict containing workflow_id / workflow. The dict form may also include base_dir, persisted_path, or path; when a persisted run path is provided, Harn derives the workflow root from the run's parent workspace automatically.

Workflow message state is persisted under .harn/workflows/<workflow_id>/state.json relative to the resolved base directory. workflow.update(...) polls for a response until options.timeout_ms elapses; the default is 30000.

Tool lifecycle hooks

FunctionParametersReturnsDescription
register_tool_hook(config)config: dictnilRegister a pre/post hook for tool calls matching pattern (glob). deny string blocks matching tools; max_output int truncates results; pre/post closures can return tool actions or reminder effects
clear_tool_hooks()nonenilRemove all registered tool hooks

Session lifecycle hooks

register_session_hook(event, handler) (or register_session_hook(event, pattern, handler)) wires a callback into the whole-session turn loop. Events: session_start, session_end, user_prompt_submit, pre_compact, post_compact, post_turn, permission_asked, permission_replied, file_edited, session_error, session_idle. The handler receives a typed event dict ({event, session: {id}, ...}) and returns:

  • nil or true — allow the operation to proceed.
  • {block: true, reason} — veto the operation (honoured for user_prompt_submit and pre_compact).
  • {decision: "allow"|"deny", reason} — short-circuit a permission_asked decision.

Each invocation is recorded on the active session transcript under the hook_call, hook_returned, and hook_vetoed event kinds, so replay tooling reproduces the same control flow. For slow background context work, return a receipt from std/context/maintenance and let the host-owned queue run the job.

FunctionParametersReturnsDescription
register_session_hook(event, pattern?, handler)event: string, pattern: string?, handler: closurenilRegister a session-level lifecycle hook
clear_session_hooks()nonenilRemove all registered session-level hooks
notify_file_edited(path, metadata?)path: string, metadata: dict?nilExplicitly queue a file_edited notification; the standard fs builtins (write_file, append_file, write_file_bytes) also queue automatically. Hooks fire at the next agent-loop turn boundary.

Reminder providers

agent_loop(...) enables canonical reminder providers by default. Use reminders: false to disable all providers or reminders: {providers: ["-token_pressure"]} to opt out by provider id. Bare llm_call(...) does not fire reminder providers.

FunctionParametersReturnsDescription
register_reminder_provider(config)config: dictnilRegister a Harn-defined reminder provider. config.id is a string, config.subscribes_to is an event string or list, and config.evaluate(ctx) returns a reminder effect/spec/list or nil.
clear_reminder_providers()nonenilRemove user-defined reminder providers. Canonical stdlib providers remain available through agent_loop unless disabled with reminders.

Context and compaction utilities

FunctionParametersReturnsDescription
assemble_context(options)options: dictdictPack artifacts into a token-budgeted slice of chunks with pluggable ranking, cross-artifact dedup, microcompact chunking, and per-chunk observability. See Adaptive context assembly below
estimate_tokens(messages)messages: listintEstimate token count for a message list (chars / 4 heuristic)
microcompact(text, max_chars?)text, max_chars (default 20000)stringSnip oversized text, keeping head and tail with a marker
select_artifacts_adaptive(artifacts, policy)artifacts: list, policy: dictlistDeduplicate, microcompact oversized artifacts, then select with token budget
transcript_auto_compact(messages, options?)messages: list, options: dictlistRun the same transcript auto-compaction pipeline used by agent_loop

std/context also provides the host-neutral harn.context_artifact.v1 envelope for durable repository context. Hosts use context_artifact(...) to normalize kind, scope/path, language, role/task applicability, freshness, confidence, provenance, source hashes, token estimate, body text, and redaction/sensitivity metadata. The companion helpers context_artifact_rank, context_artifact_dedupe, context_artifact_merge, context_artifact_budget, and context_artifact_select implement the portable rank/dedupe/merge/budget pass before a host feeds selected artifacts into assemble_context or directly into a prompt.

context_render_artifacts(...) renders the same envelope as Markdown, XML, plain text, or compact lines. With variant: "auto", it uses the same provider capability flags as logical prompt sections: prefers_xml_scaffolding selects XML and prefers_markdown_scaffolding selects Markdown. Existing Burin .burin/context-digests markdown files can be wrapped in-place with context_artifact_from_burin_digest(path, body, options?) during migration.

Adaptive context assembly

assemble_context is the within-selection complement to transcript_auto_compact. Where transcript compaction shrinks an ongoing conversation, assemble_context re-packs the next turn's artifacts into a fixed token budget:

  1. Chunk oversized artifacts at paragraph / line boundaries.
  2. Dedup chunks across artifacts (exact text match or trigram Jaccard).
  3. Rank by recency, keyword overlap against query, or a host ranker closure.
  4. Pack greedy into budget_tokens, reporting why each chunk was included or dropped.

Options dict:

KeyTypeDefaultMeaning
artifactslist[artifact]requiredSource set. Each entry is normalized via artifact(...).
budget_tokensint8000Hard cap on packed tokens.
dedup"none" / "chunked" / "semantic""chunked"Exact-text hash ("chunked") or trigram Jaccard overlap ("semantic").
semantic_overlapfloat0.85Jaccard threshold when dedup: "semantic".
strategy"recency" / "relevance" / "round_robin""relevance"Packing order.
querystringnilUsed by the default relevance ranker (keyword overlap + density).
microcompact_thresholdint2000Artifacts above this many tokens are chunked.
ranker_callbackclosure(query, chunks) → list[float]nilHost-supplied ranker. Returns a score per chunk in the same order as the chunks input. Only invoked when strategy: "relevance".

Returned record:

  • chunks: list[chunk] — selected chunks in pack order. Each carries id, artifact_id, artifact_kind, title, source, text, estimated_tokens, chunk_index, chunk_count, and score. chunk.id = "{artifact_id}#{sha256(text)[..16]}" — stable and content-addressed, so the same input always produces the same id across runs for replay diffing.
  • included: list[summary] — per-artifact {artifact_id, artifact_kind, chunks_included, chunks_total, tokens_included}.
  • dropped: list[exclusion] — per-exclusion {artifact_id, chunk_id, reason, detail}. Reasons include "no_text", "empty_text", "duplicate", "budget_exceeded".
  • reasons: list[rationale] — per-chunk {chunk_id, artifact_id, strategy, score, included, reason}. Use this to surface "why was this in the prompt?" in observability dashboards.
  • total_tokens, budget_tokens, strategy, dedup echo the packing configuration for downstream tooling.

Integration hook: a workflow node may carry context_assembler: {...} in its declaration. When set, execute_stage_node routes the pre-selected artifacts through assemble_context and renders the packed chunks as the stage's prompt context, replacing the default render_artifacts_context output. Scripts that call agent_loop directly can do the same manually: call assemble_context on their artifact list and bake the packed chunks into the system prompt.

Delegated workers

FunctionParametersReturnsDescription
spawn_agent(config)config: dictdictStart a worker from a workflow graph or delegated stage config
sub_agent_request(task, options?)task: string, options: dictdictBuild the normalized child-agent request that sub_agent_run sends to the host execution primitive
sub_agent_run(task, options?)task: string, options: dictdictRun an isolated child agent loop and return a clean envelope {ok, summary, artifacts, evidence_added, tokens_used, budget_exceeded, data, error, session_id, transcript}; with background: true, returns an agent-handle summary
agent_lifecycle_tools(registry?, options?)registry: dict or nil, options: dict or nildictAdd model-facing lifecycle tools. Always registers agent_await_resumption; registers subagent_pause, subagent_resume, and subagent_stop when {subagents: true} / {subagent_tools: true} is set or the registry already exposes a subagent tool
send_input(handle, task)handle, taskdictRe-run a completed worker with a new task, carrying forward worker state where applicable
suspend_agent(worker, reason?, options?)worker, reason, optionsdictCooperatively suspend a worker, persist a resumable snapshot, and return status: "suspended" with suspension metadata
resume_agent(worker_or_snapshot, resume_input?, continue_transcript?)worker or snapshot, input, booldictResume a suspended worker, optionally with new input; set continue_transcript=false to resume from the prior summary plus new input only
agent_stop(worker, options?)worker, optionsdictStop a worker. {graceful: true} returns a recursive handoff summary before emitting WorkerStopped; omitted or false hard-cancels
parse_resume_conditions(conditions?)conditionsdict or nilValidate and normalize ResumeConditions for self-parking agents and spawn_agent({options: {resume_when}}) using the trigger-spec validator
wait_agent(handle_or_list)handle or listdict or listWait for one worker or a list of workers to finish
close_agent(handle)handledictCancel a worker and mark it terminal
list_agents()nonelistList worker summaries tracked by the current runtime

spawn_agent(...) accepts either:

  • {task, graph, artifacts?, options?, name?, wait?} for typed workflow runs
  • {task, node, artifacts?, transcript?, name?, wait?} for delegated stage runs
  • Either shape may also include policy: <capability_policy> to narrow the worker's inherited execution ceiling.
  • Either shape may also include tools: ["name", ...] as shorthand for a worker policy that only allows those tool names.
  • Either shape may also include execution: {cwd?, env?, worktree?} where worktree accepts {repo, path?, branch?, base_ref?, cleanup?}.
  • Either shape may also include audit: {session_id?, parent_session_id?, mutation_scope?, approval_policy?}

Worker configs may also include carry to control continuation behavior:

  • carry: {artifacts: "inherit" | "none" | <context_policy>}
  • carry: {resume_workflow?: bool, persist_state?: bool}

To give a spawned worker prior conversation context, open a session before spawning and set model_policy.session_id on the worker's node. Use agent_session_fork(parent) if the worker should start from a branch of an existing conversation; agent_session_reset(id) before the call if you want a fresh run with the same id.

Workers return handle dicts with an id, lifecycle timestamps, status, mode, result/error fields, transcript presence, produced artifact count, snapshot/child-run paths, immutable original request metadata, normalized provenance, and audit mutation-session metadata when available. The request object preserves canonical research_questions, action_items, workflow_stages, and verification_steps arrays when the caller supplied them. When a worker-scoped policy denies a tool call, the agent receives a structured tool result payload: {error: "permission_denied", tool: "...", reason: "..."}.

sub_agent_run(task, options?) is the lighter-weight context-firewall primitive. It builds a Harn-owned sub_agent_request(...), starts a child session through the host execution primitive, runs a full agent_loop, and returns only a single typed envelope to the parent:

  • summary, artifacts, evidence_added, tokens_used, budget_exceeded, session_id, and optional data
  • ok: false plus error: {category, message, tool?} when the child fails or hits a capability denial
  • background: true returns a normal worker handle whose mode is sub_agent

Options mirror agent_loop where relevant (provider, model, tools, tool_format, max_iterations, token_budget, policy, approval_policy, session_id, system) and also accept:

  • allowed_tools: ["name", ...] to narrow the child tool registry and capability ceiling
  • reminder_propagation: [...] to explicitly seed inherited system reminders; when omitted, pending parent reminders are filtered by their propagate policy and inherited automatically
  • response_format: "json" to parse structured child JSON into data from the final successful transcript when possible
  • returns: {schema: ...} to validate that structured child JSON against a schema

Artifacts and context

FunctionParametersReturnsDescription
artifact(payload)payload: dictartifactNormalize a typed artifact/resource
artifact_emit(kind, spec, options?)kind, spec, optionsdictValidate and emit a renderable Vega-Lite, Mermaid, or table artifact event
artifact_derive(parent, kind, extra?)parent, kind, extraartifactDerive a new artifact from a prior one
artifact_select(artifacts, policy?)artifacts, policylistSelect artifacts under context policy and budget
artifact_context(artifacts, policy?)artifacts, policystringRender selected artifacts into context
artifact_workspace_file(path, content, extra?)path, content, extraartifactBuild a normalized workspace-file artifact with path provenance
artifact_workspace_snapshot(paths, summary?, extra?)paths, summary, extraartifactBuild a workspace snapshot artifact for host/editor context
artifact_editor_selection(path, text, extra?)path, text, extraartifactBuild an editor-selection artifact from host UI state
artifact_verification_result(title, text, extra?)title, text, extraartifactBuild a verification-result artifact
artifact_test_result(title, text, extra?)title, text, extraartifactBuild a test-result artifact
artifact_command_result(command, output, extra?)command, output, extraartifactBuild a command-result artifact with structured output
artifact_diff(path, before, after, extra?)path, before, after, extraartifactBuild a unified diff artifact from before/after text
artifact_git_diff(diff_text, extra?)diff_text, extraartifactBuild a git-diff artifact from host/tool output
artifact_diff_review(target, summary?, extra?)target, summary, extraartifactBuild a diff-review artifact linked to a diff/patch target
artifact_review_decision(target, decision, extra?)target, decision, extraartifactBuild an accept/reject review-decision artifact linked by lineage
artifact_patch_proposal(target, patch, extra?)target, patch, extraartifactBuild a proposed patch artifact linked to an existing target
artifact_verification_bundle(title, checks, extra?)title, checks, extraartifactBundle structured verification checks into one review artifact
artifact_apply_intent(target, intent, extra?)target, intent, extraartifactRecord an apply or merge intent linked to a reviewed artifact

Core artifact kinds commonly used by the runtime include resource, workspace_file, workspace_snapshot, editor_selection, summary, transcript_summary, diff, git_diff, patch, patch_set, patch_proposal, diff_review, review_decision, verification_bundle, apply_intent, test_result, verification_result, command_result, and plan.

Sessions

Sessions are the first-class resource for agent-loop conversations. They own a transcript history, closure subscribers, and a lifecycle. See the Sessions chapter for the full model.

FunctionParametersReturnsDescription
agent_session_open(id?)id: string or nilstringIdempotent open; nil mints a UUIDv7
agent_session_exists(id)idboolSafe on unknown ids
agent_session_current_id()nonestring or nilReturns the innermost active session id, or nil outside any active session
agent_session_length(id)idintMessage count; errors on unknown id
agent_session_snapshot(id)iddict or nilRead-only transcript snapshot plus length, created_at, system_prompt, tool_format, scratchpad, scratchpad_version, parent_id, child_ids, and branched_at_event_index
agent_session_ancestry(id)iddict or nilReturns {parent_id, child_ids, root_id} for the current in-VM lineage
agent_session_reset(id)idnilWipes history; preserves id and subscribers
agent_session_fork(src, dst?)src, dststringCopies transcript, sets dst.parent_id, and appends dst to src.child_ids
agent_session_fork_at(src, keep_first, dst?)src, keep_first: int, dststringForks then keeps the first keep_first messages on the child; records branched_at_event_index
agent_session_scratchpad(id)iddict or nilReturns the small session-local agent scratchpad
agent_session_set_scratchpad(id, scratchpad, opts?)id, scratchpad: dict, opts: dictdictStores a dict scratchpad and returns {ok, version, scratchpad}. opts may include source, reason, and metadata
agent_session_clear_scratchpad(id, opts?)id, opts: dictdictClears the scratchpad and returns {ok, version, scratchpad: nil}
agent_session_trim(id, keep_last)id, keep_last: intintRetain last keep_last messages; returns kept count
agent_session_compact(id, opts)id, opts: dictintRuns the LLM/truncate/observation-mask/custom compactor; custom strategies use custom_compactor, mask_callback, or compress_callback closures
agent_session_inject(id, message)id, message: dictnilAppends {role, content, …}; missing role errors
agent_session_seed_from_jsonl(jsonl_path, opts?)jsonl_path: string, opts: dictdictCreate a new session from a replayable llm_transcript.jsonl sidecar. Options: truncate_to_last, drop_tool_calls, rename_session, validate, provider, model
agent_session_close(id, status?)id, optional status string/dictnilEvicts immediately regardless of LRU cap and records an agent_session_closed event with the close reason

Pair with agent_loop(..., {session_id: id, ...}): prior messages load as prefix and the final transcript is persisted back on exit.

Transcript lifecycle

Lower-level transcript primitives. Most callers should prefer sessions; these remain useful for building synthetic transcripts, replay fixtures, and offline analysis.

FunctionParametersReturnsDescription
transcript(metadata?)metadata: anytranscriptCreate an empty transcript
transcript_messages(transcript)transcriptlistReturn transcript messages
transcript_assets(transcript)transcriptlistReturn transcript asset descriptors
transcript_add_asset(transcript, asset)transcript, assettranscriptRegister a durable asset reference on a transcript
transcript_events(transcript)transcriptlistReturn canonical transcript events
transcript_events_by_kind(transcript, kind)transcript, kindlistFilter transcript events by their kind field
transcript_reminder_event(reminder)reminder: dictevent dictBuild a normalized system_reminder event (see System reminders). Reminder injection and clearing live in Transcript helpers above.
transcript_suspension_event(suspension)suspension: dictevent dictBuild a normalized suspension lifecycle event
transcript_resumption_event(resumption)resumption: dictevent dictBuild a normalized resumption lifecycle event
transcript_drain_decision_event(drain)drain: dictevent dictBuild a normalized drain_decision lifecycle event
transcript_stats(transcript)transcriptdictCount messages, tool calls, and visible events on a transcript
transcript_summary(transcript)transcriptstring or nilReturn transcript summary
transcript_fork(transcript, options?)transcript, optionstranscriptFork transcript state
transcript_reset(options?)optionstranscriptStart a fresh active transcript with optional metadata
transcript_archive(transcript)transcripttranscriptMark transcript archived and append an internal lifecycle event
transcript_abandon(transcript)transcripttranscriptMark transcript abandoned and append an internal lifecycle event
transcript_resume(transcript)transcripttranscriptMark transcript active again and append an internal lifecycle event
transcript_compact(transcript, options?)transcript, optionstranscriptCompact a transcript with the runtime compaction engine, including reminder TTL/dedupe/preserve handling and strategy: "custom" plus custom_compactor(messages, reminders)
transcript_summarize(transcript, options?)transcript, optionstranscriptCompact via LLM-generated summary
transcript_auto_compact(messages, options?)messages, optionslistApply the agent-loop compaction pipeline to a message list
transcript_render_visible(transcript)transcriptstringRender only public/human-visible messages
transcript_render_full(transcript)transcriptstringRender the full execution history

Transcript messages may now carry structured block content instead of plain text. Use add_user(...), add_assistant(...), or add_message(...) with a list of blocks such as {type: "text", text: "..."}, {type: "image", asset_id: "..."}, {type: "file", asset_id: "..."}, and {type: "tool_call", ...}, with per-block visibility: "public" | "internal" | "private". Durable media belongs in transcript.assets, while message/event blocks should reference those assets by id or path.