// @harn-entrypoint-category agent.stdlib
//
// std/agent/stack — canonical composition helpers for the common "agent
// runtime cell": model/provider defaults, capability-aware option cleanup,
// caller middleware, and tool middleware. This module intentionally composes
// existing stdlib primitives instead of introducing a second routing/catalog
// surface.
import { agent_tool_format_resolution } from "std/agent/options"
import { pick_keys } from "std/collections"
import { pack_for } from "std/llm/defaults"
import {
compose,
default_llm_caller,
with_budget,
with_cache,
with_logging,
with_retry,
} from "std/llm/handlers"
import { natural_language_executor_middleware } from "std/llm/tool_binder"
import {
compose_tool_callers,
tools_use_middleware,
with_required_reason,
} from "std/llm/tool_middleware"
fn __stack_dict(value) {
if type_of(value) == "dict" {
return value
}
return {}
}
fn __stack_list(value) {
if type_of(value) == "list" {
return value
}
return []
}
fn __stack_is_callable(value) -> bool {
let kind = type_of(value)
return kind == "function" || kind == "closure" || kind == "fn"
}
fn __stack_has_key(d, key) -> bool {
return type_of(d) == "dict" && contains(d.keys(), key)
}
fn __stack_non_empty_string(value) {
if type_of(value) != "string" {
return nil
}
let trimmed = trim(value)
if trimmed == "" {
return nil
}
return trimmed
}
fn __stack_env_token(value) -> string {
let text = __stack_non_empty_string(value)
if text == nil {
return ""
}
let cleaned = regex_replace_all("[^A-Za-z0-9]+", "_", text)
return uppercase(regex_replace_all("^_+|_+$", "", cleaned))
}
fn __stack_env_value(name) {
let key = __stack_non_empty_string(name)
if key == nil {
return nil
}
let value = harness.env.get(key)
return __stack_non_empty_string(value)
}
fn __stack_first_non_empty(values) {
for value in values {
let text = __stack_non_empty_string(value)
if text != nil {
return text
}
}
return nil
}
fn __stack_bool_value(value, fallback) -> bool {
if type_of(value) == "bool" {
return value
}
if type_of(value) == "string" {
let normalized = lowercase(trim(value))
if normalized == "1" || normalized == "true" || normalized == "yes" || normalized == "y"
|| normalized == "on" {
return true
}
if normalized == "0" || normalized == "false" || normalized == "no" || normalized == "n"
|| normalized == "off" {
return false
}
}
return fallback
}
fn __stack_env_prefixes(config, role) {
let cfg = __stack_dict(config)
var prefixes = []
let explicit = cfg?.env_prefixes ?? cfg?.env_prefix
if type_of(explicit) == "list" {
for prefix in explicit {
let token = __stack_env_token(prefix)
if token != "" && !contains(prefixes, token) {
prefixes = prefixes + [token]
}
}
} else {
let token = __stack_env_token(explicit)
if token != "" {
prefixes = prefixes + [token]
}
}
let role_token = __stack_env_token(role)
if role_token != "" {
for prefix in ["HARN_AGENT_" + role_token, "HARN_LLM_" + role_token, "HARN_" + role_token] {
if !contains(prefixes, prefix) {
prefixes = prefixes + [prefix]
}
}
}
for prefix in ["HARN_AGENT", "HARN_LLM"] {
if !contains(prefixes, prefix) {
prefixes = prefixes + [prefix]
}
}
return prefixes
}
fn __stack_first_env(prefixes, suffix) {
for prefix in prefixes {
let value = __stack_env_value(prefix + "_" + suffix)
if value != nil {
return value
}
}
return nil
}
fn __stack_drop_keys(d, keys) {
var out = __stack_dict(d)
for key in keys {
if __stack_has_key(out, key) {
out = out.remove(key)
}
}
return out
}
fn __stack_provider_model_pair(opts) {
let provider = __stack_non_empty_string(opts?.provider)
let model = __stack_non_empty_string(opts?.model)
if provider != nil && model != nil {
return {provider: provider, model: model}
}
if model != nil {
let info = try {
llm_model_info(model)
}
if !is_err(info) && type_of(unwrap(info)) == "dict" {
let resolved = unwrap(info)
return {
provider: __stack_non_empty_string(resolved?.provider) ?? provider ?? "auto",
model: __stack_non_empty_string(resolved?.id ?? resolved?.model) ?? model,
}
}
}
return nil
}
fn __stack_capabilities(opts) {
let pair = __stack_provider_model_pair(opts)
if pair == nil {
return nil
}
let caps = try {
provider_capabilities(pair.provider ?? "auto", pair.model ?? "")
}
if is_err(caps) || type_of(unwrap(caps)) != "dict" {
return nil
}
return unwrap(caps)
}
fn __stack_sanitize_mode(policy) {
let raw = __stack_non_empty_string(__stack_dict(policy)?.mode)
if raw == nil {
return "request"
}
return lowercase(raw)
}
fn __stack_thinking_mode(value) {
if value == nil {
return "disabled"
}
let kind = type_of(value)
if kind == "bool" {
if value {
return "enabled"
}
return "disabled"
}
if kind == "string" {
let raw = lowercase(trim(value))
if raw == "disabled" || raw == "off" || raw == "none" {
return "disabled"
}
if raw == "adaptive" {
return "adaptive"
}
if raw == "minimal" || raw == "low" || raw == "medium" || raw == "high" || raw == "xhigh" {
return "effort"
}
return "enabled"
}
if kind == "dict" {
if __stack_has_key(value, "enabled") && !(value.enabled ?? false) {
return "disabled"
}
let raw = lowercase(to_string(value?.mode ?? "enabled"))
if raw == "disabled" || raw == "off" || raw == "none" {
return "disabled"
}
if raw == "adaptive" {
return "adaptive"
}
if raw == "effort" {
return "effort"
}
return "enabled"
}
return "enabled"
}
fn __stack_thinking_supported(caps, thinking) {
let mode = __stack_thinking_mode(thinking)
if mode == "disabled" {
return true
}
let modes = __stack_list(caps?.thinking_modes)
if mode == "enabled" {
return contains(modes, "enabled") || contains(modes, "adaptive")
}
return contains(modes, mode)
}
fn __stack_strip_option_keys(options, stripped, keys) {
var out = options
var removed = stripped
for key in keys {
if __stack_has_key(out, key) {
out = out.remove(key)
removed = removed + [key]
}
}
return {options: out, stripped: removed}
}
fn __stack_sanitize_result(options, policy = nil) {
var out = __stack_dict(options)
var stripped = []
let mode = __stack_sanitize_mode(policy)
if mode == "healthcheck" || mode == "preflight" || mode == "probe" {
let deliberation = __stack_strip_option_keys(out, stripped, ["reasoning_effort", "thinking", "effort"])
out = deliberation.options
stripped = deliberation.stripped
}
let caps = __stack_capabilities(out)
if caps == nil {
return {options: out, stripped: stripped}
}
if __stack_has_key(out, "reasoning_effort") && !(caps?.reasoning_effort_supported ?? false) {
out = out.remove("reasoning_effort")
stripped = stripped + ["reasoning_effort"]
}
if __stack_has_key(out, "thinking") && !__stack_thinking_supported(caps, out.thinking) {
out = out.remove("thinking")
stripped = stripped + ["thinking"]
}
if !(caps?.prompt_caching ?? false) {
let cache = __stack_strip_option_keys(out, stripped, ["prompt_cache", "prompt_caching", "cache_control"])
out = cache.options
stripped = cache.stripped
}
return {options: out, stripped: stripped}
}
fn __stack_pack_options(base) {
let opts = __stack_dict(base)
if __stack_non_empty_string(opts?.model) == nil {
return opts
}
let packed = try {
pack_for(opts)
}
if is_err(packed) {
return opts
}
return unwrap(packed)
}
fn __stack_configured_model_options(cfg, prefixes) {
let defaults = __stack_dict(cfg?.defaults) + __stack_dict(cfg?.model_defaults)
let user = __stack_dict(cfg?.options) + __stack_dict(cfg?.llm_options)
let provider = __stack_first_non_empty(
[cfg?.provider, user?.provider, __stack_first_env(prefixes, "PROVIDER"), defaults?.provider],
)
let model = __stack_first_non_empty(
[cfg?.model, user?.model, __stack_first_env(prefixes, "MODEL"), defaults?.model],
)
let model_role = __stack_first_non_empty(
[cfg?.model_role, user?.model_role, __stack_first_env(prefixes, "MODEL_ROLE"), defaults?.model_role],
)
let task = __stack_first_non_empty(
[cfg?.task, user?.task, __stack_first_env(prefixes, "TASK"), defaults?.task],
)
let effort = __stack_first_non_empty(
[cfg?.effort, user?.effort, __stack_first_env(prefixes, "EFFORT"), defaults?.effort],
)
let tool_format = __stack_first_non_empty(
[
cfg?.tool_format,
user?.tool_format,
__stack_first_env(prefixes, "TOOL_FORMAT"),
defaults?.tool_format,
],
)
var base = defaults + user
if provider != nil {
base = base + {provider: provider}
}
if model != nil {
base = base + {model: model}
}
if model_role != nil {
base = base + {model_role: model_role}
}
if task != nil {
base = base + {task: task}
}
if effort != nil {
base = base + {effort: effort}
}
if tool_format != nil {
base = base + {tool_format: tool_format}
}
return base
}
/**
* Return a capability-sanitized options dict for a provider/model route.
* Unsupported provider-specific knobs are dropped instead of leaking to the
* wire. Currently strips unsupported `reasoning_effort`, `thinking`, and prompt-cache
* request keys. Pass `{mode: "healthcheck"}` (or `"preflight"` / `"probe"`)
* to also drop deliberate-reasoning knobs for cheap readiness probes even
* when the full route supports them.
*
* @effects: []
* @errors: []
* @api_stability: experimental
*/
pub fn agent_sanitize_model_options(options = nil, policy = nil) {
return __stack_sanitize_result(options, policy).options
}
/**
* Resolve model options for an agent role.
*
* Precedence: explicit config/options > env prefixes > defaults. Env prefixes
* are supplied via `env_prefix`/`env_prefixes`; when omitted, role-derived
* prefixes such as `HARN_AGENT_PLANNER`, `HARN_LLM_PLANNER`, and
* `HARN_PLANNER` are checked before shared `HARN_AGENT`/`HARN_LLM`.
*
* Returns `{role, env_prefixes, provider, model, options, tool_format,
* tool_format_source, stripped, audit}`.
*
* @effects: [env.read]
* @errors: []
* @api_stability: experimental
*/
pub fn agent_model_options(config = nil) {
let cfg = __stack_dict(config)
let role = __stack_non_empty_string(cfg?.role) ?? "agent"
let prefixes = __stack_env_prefixes(cfg, role)
let configured = __stack_configured_model_options(cfg, prefixes)
let packed = __stack_pack_options(configured)
let sanitized = __stack_sanitize_result(packed)
var options = sanitized.options
let resolution = agent_tool_format_resolution(options)
if resolution?.tool_format != nil {
options = options + {tool_format: resolution.tool_format}
}
return {
role: role,
env_prefixes: prefixes,
provider: options?.provider,
model: options?.model,
model_role: options?.model_role,
options: options,
tool_format: options?.tool_format,
tool_format_source: resolution?.source ?? "unresolved",
stripped: sanitized.stripped,
audit: {
kind: "agent_stack.model_options",
role: role,
provider: options?.provider,
model: options?.model,
tool_format: options?.tool_format,
stripped: sanitized.stripped,
},
}
}
fn __stack_middleware_enabled(value, fallback) -> bool {
if value == nil {
return fallback
}
if type_of(value) == "dict" {
return true
}
return __stack_bool_value(value, fallback)
}
fn __stack_middleware_options(value, fallback = nil) {
if type_of(value) == "dict" {
return value
}
if type_of(fallback) == "dict" {
return fallback
}
return {}
}
/**
* Build the canonical LLM caller stack for agent_loop.
*
* Default behavior includes bounded transient retry. Disable with
* `{retry: false}`. Optional `logging`, `cache`, and `budget` entries compose
* the existing `std/llm/handlers` middleware. Returns `{caller, wrappers}`.
*
* @effects: []
* @errors: []
* @api_stability: experimental
*/
pub fn agent_llm_caller(config = nil) {
let cfg = __stack_dict(config)
let base = if __stack_is_callable(cfg?.llm_caller) {
cfg.llm_caller
} else {
default_llm_caller()
}
var wrappers = []
var names = []
if type_of(cfg?.wrappers) == "list" {
for wrapper in cfg.wrappers {
wrappers = wrappers + [wrapper]
names = names + ["custom"]
}
}
if __stack_middleware_enabled(cfg?.retry, true) {
let opts = __stack_middleware_options(cfg?.retry, {max_attempts: 3})
wrappers = wrappers + [{ next -> return with_retry(next, opts) }]
names = names + ["retry"]
}
if __stack_middleware_enabled(cfg?.logging, false) {
let opts = __stack_middleware_options(cfg?.logging)
wrappers = wrappers + [{ next -> return with_logging(next, opts) }]
names = names + ["logging"]
}
if __stack_middleware_enabled(cfg?.cache, false) {
let opts = __stack_middleware_options(cfg?.cache)
wrappers = wrappers + [{ next -> return with_cache(next, opts) }]
names = names + ["cache"]
}
if __stack_middleware_enabled(cfg?.budget, false) {
let opts = __stack_middleware_options(cfg?.budget)
wrappers = wrappers + [{ next -> return with_budget(next, opts) }]
names = names + ["budget"]
}
let apply_wrappers = compose(wrappers)
return {caller: apply_wrappers(base), wrappers: names}
}
fn __stack_binder_env_options(raw) {
let cfg = __stack_dict(raw)
let provider = __stack_first_non_empty(
[cfg?.provider, __stack_env_value("HARN_BINDER_PROVIDER"), cfg?.default_provider],
)
let model = __stack_first_non_empty([cfg?.model, __stack_env_value("HARN_BINDER_MODEL"), cfg?.default_model])
var out = cfg
if provider != nil {
out = out + {provider: provider}
}
if model != nil {
out = out + {model: model}
}
let timeout = __stack_env_value("HARN_BINDER_TIMEOUT_MS")
if timeout != nil && out?.timeout_ms == nil {
out = out + {timeout_ms: to_int(timeout)}
}
let max_tokens = __stack_env_value("HARN_BINDER_MAX_TOKENS")
if max_tokens != nil && out?.max_tokens == nil {
out = out + {max_tokens: to_int(max_tokens)}
}
return __stack_drop_keys(out, ["default_provider", "default_model", "enable_when_env"])
}
fn __stack_binder_enabled(raw) -> bool {
let env_switch = __stack_env_value("HARN_BINDER")
if env_switch != nil {
return __stack_bool_value(env_switch, false)
}
if raw == nil {
return false
}
if type_of(raw) == "bool" {
return raw
}
if type_of(raw) == "dict" {
let gate = __stack_non_empty_string(raw?.enable_when_env)
if gate != nil && __stack_env_value(gate) == nil {
return false
}
return true
}
return false
}
/**
* Build a tool registry plus `tool_caller` middleware stack.
*
* Supports the two most common project-wide middleware pairs:
* `required_reason` and the natural-language `binder`. Additional callers may
* be supplied as `tool_callers`. Returns `{tools, tool_caller, wrappers}`.
*
* @effects: [env.read]
* @errors: []
* @api_stability: experimental
*/
pub fn agent_tool_stack(tools = nil, config = nil) {
let cfg = __stack_dict(config)
var registry = tools ?? cfg?.tools
var callers = []
var wrappers = []
if type_of(cfg?.tool_callers) == "list" {
for caller in cfg.tool_callers {
callers = callers + [caller]
wrappers = wrappers + ["custom"]
}
}
if __stack_is_callable(cfg?.tool_caller) {
callers = callers + [cfg.tool_caller]
wrappers = wrappers + ["custom"]
}
if __stack_middleware_enabled(cfg?.required_reason, false) {
let mw = with_required_reason(__stack_middleware_options(cfg?.required_reason))
if registry != nil {
registry = tools_use_middleware(registry, mw.schema_transform)
}
callers = callers + [mw.caller]
wrappers = wrappers + ["required_reason"]
}
if __stack_binder_enabled(cfg?.binder) {
let binder_opts = __stack_binder_env_options(cfg?.binder)
let mw = natural_language_executor_middleware(binder_opts)
if registry != nil {
registry = tools_use_middleware(registry, mw.schema_transform)
}
callers = callers + [mw.caller]
wrappers = wrappers + ["binder"]
}
let caller = if len(callers) > 0 {
compose_tool_callers(callers)
} else {
nil
}
return {tools: registry, tool_caller: caller, wrappers: wrappers}
}
/**
* Build a complete agent_loop options bundle.
*
* Returns `{role, provider, model, options, llm_caller, tool_caller, tools,
* audit}`. The `options` dict is ready to pass to `agent_loop` and includes
* the generated callers/tools when present.
*
* @effects: [env.read]
* @errors: []
* @api_stability: experimental
*/
pub fn agent_stack(config = nil) {
let cfg = __stack_dict(config)
let model = agent_model_options(cfg)
let llm = agent_llm_caller(cfg)
let tool_stack = agent_tool_stack(cfg?.tools, cfg)
let final_model = __stack_sanitize_result(__stack_pack_options(model.options + __stack_dict(cfg?.agent_options)))
let stripped = __stack_list(model.stripped) + __stack_list(final_model.stripped)
var options = final_model.options
options = options + {llm_caller: llm.caller}
if tool_stack.tools != nil {
options = options + {tools: tool_stack.tools}
}
if tool_stack.tool_caller != nil {
options = options + {tool_caller: tool_stack.tool_caller}
}
return {
role: model.role,
provider: options?.provider,
model: options?.model,
tool_format: options?.tool_format,
options: options,
llm_caller: llm.caller,
tool_caller: tool_stack.tool_caller,
tools: tool_stack.tools,
audit: {
kind: "agent_stack",
model: model.audit
+ {
provider: options?.provider,
model: options?.model,
tool_format: options?.tool_format,
stripped: stripped,
},
llm_wrappers: llm.wrappers,
tool_wrappers: tool_stack.wrappers,
},
}
}
/**
* Render a compact, stable audit line for receipts/logging.
*
* @effects: []
* @errors: []
* @api_stability: experimental
*/
pub fn agent_stack_audit_line(stack) -> string {
let audit = __stack_dict(stack?.audit)
let model = __stack_dict(audit?.model)
let role = to_string(stack?.role ?? model?.role ?? "agent")
let provider = to_string(stack?.provider ?? model?.provider ?? "<auto>")
let selected = to_string(stack?.model ?? model?.model ?? "<auto>")
let format = to_string(stack?.tool_format ?? model?.tool_format ?? "<auto>")
let llm_wrappers = join(__stack_list(audit?.llm_wrappers), ",")
let tool_wrappers = join(__stack_list(audit?.tool_wrappers), ",")
return "agent_stack role=" + role + " provider=" + provider + " model=" + selected
+ " tool_format="
+ format
+ " llm=["
+ llm_wrappers
+ "] tools=["
+ tool_wrappers
+ "]"
}
/**
* Pick the stable option keys most workflow stages should copy into
* `model_policy`.
*
* @effects: []
* @errors: []
* @api_stability: experimental
*/
pub fn agent_stack_model_policy(stack) {
let options = __stack_dict(stack?.options ?? stack)
return pick_keys(
options,
[
"provider",
"model",
"model_role",
"temperature",
"max_tokens",
"thinking",
"reasoning_effort",
"reasoning_policy",
"thinking_policy",
"reasoning_scale",
"problem_scale",
"reasoning_task",
"tool_format",
"native_tool_fallback",
"iteration_budget",
"max_iterations",
"max_nudges",
"stop_after_successful_tools",
"require_successful_tools",
],
{drop_nil: true},
)
}