// @harn-entrypoint-category agent.stdlib
import { agent_autocompact_if_needed } from "std/agent/autocompact"
import { agent_budget_post_call_blocked, agent_budget_pre_call_blocked } from "std/agent/budget"
import { agent_daemon_snapshot, agent_daemon_step } from "std/agent/daemon"
import { agent_verify_or_continue } from "std/agent/judge"
import { agent_mcp_bootstrap_if_needed } from "std/agent/mcp"
import { agent_loop_options } from "std/agent/options"
import { agent_compute_post_turn } from "std/agent/postturn"
import { agent_build_turn_messages, agent_build_turn_system } from "std/agent/preflight"
import { agent_dispatch_tool_batch, agent_dispatch_tool_call } from "std/agent/primitives"
import { agent_progress_apply_options } from "std/agent/progress"
import { native_tool_contract_feedback_prompt } from "std/agent/prompts"
import { agent_skills_match } from "std/agent/skills"
import {
agent_emit_event,
agent_record_native_tool_fallback,
agent_session_drain_feedback,
agent_session_finalize,
agent_session_init,
agent_session_inject_feedback,
agent_session_record_assistant,
agent_session_record_tool_results,
agent_session_record_usage,
} from "std/agent/state"
import {
agent_tool_search_emit_queries,
agent_tool_search_inject_if_needed,
agent_tool_search_record_results,
} from "std/agent/tool_search"
import { llm_call_options } from "std/llm/options"
fn __default_invoke_llm(message, turn_system, llm_opts) {
let result = try {
llm_call(message, turn_system, llm_call_options(llm_opts))
}
if !is_err(result) {
return {ok: true, value: unwrap(result)}
}
let err = unwrap_err(result)
let normalized = __llm_provider_error(err, llm_opts)
let reason = if type_of(err) == "dict" {
err?.reason ?? ""
} else {
""
}
if reason == "budget_exceeded" {
return {ok: false, status: "budget_exhausted", stop_reason: "budget_exhausted", error: normalized}
}
return {ok: false, status: "provider_error", stop_reason: "provider_error", error: normalized}
}
fn __messages_include_tool_result(messages) {
if type_of(messages) != "list" {
return false
}
for msg in messages {
let role = to_string(msg?.role ?? "")
if role == "tool" || role == "tool_result" {
return true
}
}
return false
}
fn __llm_error_field(err, key, fallback) {
if type_of(err) == "dict" {
let value = err[key]
if value != nil {
return to_string(value)
}
}
return fallback
}
fn __llm_provider_error(err, llm_opts) {
let message = __llm_error_field(err, "message", to_string(err))
let category = __llm_error_field(err, "category", error_category(err))
let reason = __llm_error_field(err, "reason", "")
let kind = __llm_error_field(err, "kind", "")
let provider = __llm_error_field(err, "provider", to_string(llm_opts?.provider ?? ""))
let model = __llm_error_field(err, "model", to_string(llm_opts?.model ?? ""))
return {
category: category,
reason: reason,
kind: kind,
provider: provider,
model: model,
message: message,
phase: "llm_call",
tool_format: to_string(llm_opts?.tool_format ?? ""),
after_tool_result: __messages_include_tool_result(llm_opts?.messages),
}
}
fn __validate_caller_result(r) {
if type_of(r) != "dict" {
throw "agent_loop: llm_caller must return a dict; got " + type_of(r)
}
if r?.ok == nil {
throw "agent_loop: llm_caller result missing `ok`"
}
if r.ok && type_of(r?.value) != "dict" {
throw "agent_loop: llm_caller returned ok=true but `value` is not a dict"
}
if !r.ok && type_of(r?.status) != "string" {
throw "agent_loop: llm_caller returned ok=false but `status` is not a string"
}
}
fn __invoke_llm(message, turn_system, llm_opts) {
let caller = llm_opts?._llm_caller
if caller == nil {
return __default_invoke_llm(message, turn_system, llm_opts)
}
let call = {
prompt: message,
system: turn_system,
opts: llm_opts,
turn: {iteration: llm_opts?._turn_iteration ?? 0, session_id: llm_opts?.session_id ?? "", attempt: 1},
}
let result = try {
caller(call)
}
if is_err(result) {
throw unwrap_err(result)
}
let r = unwrap(result)
__validate_caller_result(r)
return r
}
// -------------------------------------------------------------------------------------------------
// Tool middleware seam — composable tool_caller (mirrors __invoke_llm).
//
// Each tool dispatch is funneled through `tool_caller(envelope, next)` when
// the agent_loop options carry one. The envelope normalizes the call shape
// so middleware doesn't have to peek at the underlying registry/schema:
//
// envelope = {
// tool_name, tool_args, call_id,
// declared_executor?, schema?, description?,
// turn: {iteration, session_id},
// }
//
// The middleware returns a dispatch-shape dict. Calling `next(envelope)`
// runs the default dispatch (with any envelope mutations the middleware
// applied — typically `tool_args` rewrites or argument stripping). Callers
// can short-circuit by returning their own dict without invoking `next`.
//
// See std/llm/tool_middleware for the userspace primitives + the bundled
// middleware library (with_required_reason, with_audit_log, …).
// -------------------------------------------------------------------------------------------------
fn __tool_registry_entry(tools, tool_name) {
if tools == nil {
return nil
}
let entries = tools?.tools
if type_of(entries) != "list" {
return nil
}
for entry in entries {
if type_of(entry) != "dict" {
continue
}
let entry_name = if entry?.name != nil {
to_string(entry.name)
} else {
let func = entry?.function
if type_of(func) == "dict" {
to_string(func?.name ?? "")
} else {
""
}
}
if entry_name == tool_name {
return entry
}
}
return nil
}
fn __tool_envelope(call, tools, options) {
let tool_name = to_string(call?.name ?? call?.tool_name ?? "")
let tool_args_raw = call?.arguments ?? call?.tool_args
let tool_args = if type_of(tool_args_raw) == "dict" {
tool_args_raw
} else {
{}
}
let entry = __tool_registry_entry(tools, tool_name)
let declared_executor = if entry == nil {
nil
} else {
let direct = entry?.executor
if direct != nil {
to_string(direct)
} else {
let func = entry?.function
if type_of(func) == "dict" && func?.executor != nil {
to_string(func.executor)
} else {
nil
}
}
}
let schema = if entry == nil {
nil
} else {
entry?.parameters ?? entry?.input_schema ?? entry?.inputSchema
}
let annotations = if entry == nil {
nil
} else {
entry?.annotations
}
let description = if entry == nil {
""
} else {
let direct = entry?.description
if direct != nil {
to_string(direct)
} else {
let func = entry?.function
if type_of(func) == "dict" && func?.description != nil {
to_string(func.description)
} else {
""
}
}
}
return {
tool_name: tool_name,
tool_args: tool_args,
call_id: to_string(call?.id ?? call?.tool_call_id ?? ""),
declared_executor: declared_executor,
schema: schema,
annotations: annotations,
description: description,
turn: {iteration: options?._turn_iteration ?? 0, session_id: to_string(options?.session_id ?? "")},
}
}
fn __default_invoke_tool(envelope, original_call, tools, options) {
let next_call = original_call
+ {name: envelope.tool_name, tool_name: envelope.tool_name, arguments: envelope.tool_args}
return agent_dispatch_tool_call(next_call, tools, options)
}
fn __validate_tool_caller_result(r) {
if type_of(r) != "dict" {
throw "agent_loop: tool_caller must return a dict; got " + type_of(r)
}
let name = r?.tool_name ?? r?.name
if name == nil || to_string(name) == "" {
throw "agent_loop: tool_caller result missing `tool_name`"
}
let ok = r?.ok
if ok == nil {
let success = r?.success
if success == nil {
let status = r?.status
if status == nil {
throw "agent_loop: tool_caller result missing `ok`/`success`/`status`"
}
}
} else if type_of(ok) != "bool" {
throw "agent_loop: tool_caller result `ok` must be a bool; got " + type_of(ok)
}
}
fn __middleware_exception_result(envelope, err) {
let err_text = to_string(err)
let observation = "[error from " + envelope.tool_name + "]\n" + err_text
+ "\n[end of "
+ envelope.tool_name
+ " error]\n"
return {
ok: false,
status: "error",
tool_name: envelope.tool_name,
tool_call_id: envelope.call_id,
arguments: envelope.tool_args,
result: nil,
rendered_result: err_text,
observation: observation,
error: err_text,
error_category: "tool_middleware_exception",
executor: nil,
}
}
fn __invoke_tool(call, tools, options) {
let caller = options?._tool_caller
if caller == nil {
return agent_dispatch_tool_call(call, tools, options)
}
let envelope = __tool_envelope(call, tools, options)
let next = { env_in -> __default_invoke_tool(env_in, call, tools, options) }
let outcome = try {
caller(envelope, next)
}
if is_err(outcome) {
let err = unwrap_err(outcome)
__maybe_emit_tool_audit(
envelope.turn.session_id,
envelope,
{layer: "tool_caller", status: "exception", error: to_string(err)},
)
return __middleware_exception_result(envelope, err)
}
let r = unwrap(outcome)
__validate_tool_caller_result(r)
__maybe_emit_tool_audit(envelope.turn.session_id, envelope, r?.audit)
return r
}
fn __maybe_emit_tool_audit(session_id, envelope, audit) {
if audit == nil {
return
}
if session_id == "" {
return
}
let _ = try {
agent_emit_event(
session_id,
"tool_call_audit",
{tool_call_id: envelope.call_id, tool_name: envelope.tool_name, audit: audit},
)
}
}
fn __visible_text(parsed, raw_text) {
if parsed?.user_response != nil && parsed.user_response != "" {
return parsed.user_response
}
if parsed?.prose != nil && parsed.prose != "" {
return parsed.prose
}
return raw_text
}
fn __resolve_tool_calls(llm_result, parsed) {
let native_calls = llm_result?.native_tool_calls ?? llm_result?.tool_calls ?? []
if len(native_calls) > 0 {
return native_calls
}
return parsed?.calls ?? []
}
fn __stall_diagnostics_config(value) {
if value == nil {
return {enabled: false}
}
if type_of(value) == "bool" {
return {
enabled: value,
threshold: 3,
inject_feedback: true,
max_feedback: 1,
exempt_tools: [],
include_arguments: false,
}
}
if type_of(value) != "dict" {
throw "agent_loop: `stall_diagnostics` must be a dict, bool, or nil; got " + type_of(value)
}
let enabled = value?.enabled
let resolved_enabled = if enabled == nil {
true
} else {
enabled
}
let threshold = value?.threshold ?? 3
let safe_threshold = if type_of(threshold) == "int" && threshold >= 2 {
threshold
} else {
3
}
let max_feedback = value?.max_feedback ?? 1
let safe_max_feedback = if type_of(max_feedback) == "int" && max_feedback >= 0 {
max_feedback
} else {
1
}
return {
enabled: resolved_enabled,
threshold: safe_threshold,
inject_feedback: value?.inject_feedback ?? true,
max_feedback: safe_max_feedback,
exempt_tools: value?.exempt_tools ?? value?.allow_repeated_tools ?? [],
include_arguments: value?.include_arguments ?? false,
}
}
fn __stall_initial_state() {
return {last_signature: "", streak: 0, warnings: [], repeated_tool_calls: 0, feedback_count: 0}
}
fn __stall_reset_state(state) {
return state + {last_signature: "", streak: 0}
}
fn __tool_call_name(call) {
return to_string(call?.name ?? call?.tool_name ?? "")
}
fn __tool_call_args(call) {
let raw = call?.arguments ?? call?.tool_args
if type_of(raw) == "dict" {
return raw
}
return {}
}
fn __tool_call_signature(call) {
let args_text = json_stringify(__tool_call_args(call))
return __tool_call_name(call) + "\n" + args_text
}
fn __stall_feedback_text(warning) {
return "Stall diagnostic: the last "
+ to_string(warning.repeat_count)
+ " tool calls repeated `"
+ warning.tool_name
+ "` with identical arguments. Use different evidence, finish, or explain why another identical call is necessary before repeating it."
}
fn __stall_warning_record(iteration, tool_name, args, signature, repeat_count, config) {
let args_text = json_stringify(args)
var record = {
iteration: iteration,
tool_name: tool_name,
repeat_count: repeat_count,
threshold: config.threshold,
arguments_digest: "sha256:" + sha256(args_text),
signature_digest: "sha256:" + sha256(signature),
}
if config.include_arguments {
record = record + {arguments: args}
}
return record
}
fn __stall_inject_feedback(session_id, warning, config, state) {
let wants_feedback = config.inject_feedback ?? true
let should_feedback = wants_feedback && state.feedback_count < config.max_feedback
if should_feedback {
agent_session_inject_feedback(session_id, "stall_diagnostics", __stall_feedback_text(warning))
return state + {feedback_count: state.feedback_count + 1}
}
return state
}
fn __stall_maybe_emit(session_id, warning, config, state, defer_feedback) {
agent_emit_event(session_id, "agent_loop_stall_warning", warning)
if defer_feedback {
return {state: state, warning: warning, feedback_deferred: true}
}
return {
state: __stall_inject_feedback(session_id, warning, config, state),
warning: warning,
feedback_deferred: false,
}
}
fn __stall_observe_tool_calls(session_id, tool_calls, iteration, raw_config, state, defer_feedback) {
let config = __stall_diagnostics_config(raw_config)
if !config.enabled {
return {state: state, enabled: false, warning: nil, feedback_deferred: false, config: config}
}
if len(tool_calls) == 0 {
return {
state: __stall_reset_state(state),
enabled: true,
warning: nil,
feedback_deferred: false,
config: config,
}
}
var next_state = state
var emitted_warning = nil
var feedback_deferred = false
for call in tool_calls {
let tool_name = __tool_call_name(call)
if tool_name == "" || contains(config.exempt_tools, tool_name) {
next_state = __stall_reset_state(next_state)
continue
}
let signature = __tool_call_signature(call)
let streak = if signature == next_state.last_signature {
next_state.streak + 1
} else {
1
}
let repeated = if streak > 1 {
next_state.repeated_tool_calls + 1
} else {
next_state.repeated_tool_calls
}
next_state = next_state
+ {last_signature: signature, streak: streak, repeated_tool_calls: repeated}
if streak == config.threshold {
let warning = __stall_warning_record(iteration, tool_name, __tool_call_args(call), signature, streak, config)
let emitted = __stall_maybe_emit(session_id, warning, config, next_state, defer_feedback)
next_state = emitted.state
if emitted_warning == nil {
emitted_warning = warning
}
feedback_deferred = feedback_deferred || emitted?.feedback_deferred ?? false
next_state = next_state + {warnings: next_state.warnings.push(warning)}
}
}
return {
state: next_state,
enabled: true,
warning: emitted_warning,
feedback_deferred: feedback_deferred,
config: config,
}
}
fn __apply_stall_result(result, stall_enabled, stall_state) {
if !stall_enabled && len(stall_state.warnings) == 0 {
return result
}
return result
+ {
repeated_tool_calls: stall_state.repeated_tool_calls,
stall_warnings: stall_state.warnings,
suspected_loop: len(stall_state.warnings) > 0,
}
}
fn __done_judge_stall_cadence(opts) {
let judge = opts?.done_judge
if type_of(judge) != "dict" {
return nil
}
let cadence = judge?.cadence
if type_of(cadence) != "dict" || cadence?.when != "stalled" {
return nil
}
return cadence
}
fn __done_judge_stall_due(opts, invocations, turn_number) {
let cadence = __done_judge_stall_cadence(opts)
if cadence == nil {
return false
}
let max_invocations = cadence?.max_invocations
if max_invocations != nil && invocations >= max_invocations {
return false
}
let min_iterations = cadence?.min_iterations_before_first
if min_iterations != nil && turn_number <= min_iterations {
return false
}
let every = cadence?.every
if every != nil && turn_number % every != 0 {
return false
}
return true
}
fn __native_fallback_feedback(policy, fallback_index) {
return native_tool_contract_feedback_prompt({policy: policy, fallback_index: fallback_index})
}
fn __detect_native_fallback(llm_result, parsed, turn_opts, fallback_index, session_id, turn_index) {
let native_calls = llm_result?.native_tool_calls ?? []
let parsed_calls = parsed?.calls ?? []
let format = turn_opts?.tool_format ?? ""
if format != "native" || len(native_calls) > 0 || len(parsed_calls) == 0 {
return {triggered: false, accepted: false, fallback_index: fallback_index, calls: nil}
}
let new_index = fallback_index + 1
let policy = turn_opts?.native_tool_fallback ?? "reject"
let accepted = if policy == "allow" {
true
} else if policy == "allow_once" {
new_index == 1
} else {
false
}
agent_record_native_tool_fallback(
session_id,
{
iteration: turn_index + 1,
accepted: accepted,
policy: policy,
fallback_index: new_index,
tool_call_count: len(parsed_calls),
},
)
if !accepted {
agent_session_inject_feedback(
session_id,
"native_tool_contract",
__native_fallback_feedback(policy, new_index),
)
}
let resolved_calls = if accepted {
parsed_calls
} else {
[]
}
return {triggered: true, accepted: accepted, fallback_index: new_index, calls: resolved_calls}
}
fn __dispatch_tool_calls(session_id, tool_calls, turn_opts) {
if len(tool_calls) == 0 {
return {dispatch: nil, turn_opts: turn_opts}
}
agent_tool_search_emit_queries(session_id, tool_calls, turn_opts)
let tools = turn_opts?.tools
let dispatch_options = {
session_id: session_id,
tool_format: turn_opts.tool_format,
policy: turn_opts?.policy,
approval_policy: turn_opts?.approval_policy,
command_policy: turn_opts?.command_policy,
permissions: turn_opts?.permissions,
_turn_iteration: turn_opts?._turn_iteration ?? 0,
_tool_caller: turn_opts?._tool_caller,
}
let caller = turn_opts?._tool_caller
let dispatch = if caller == nil {
agent_dispatch_tool_batch(tool_calls, tools, dispatch_options)
} else {
__dispatch_tool_calls_with_middleware(tool_calls, tools, dispatch_options)
}
agent_session_record_tool_results(session_id, dispatch)
return {
dispatch: dispatch,
turn_opts: agent_tool_search_record_results(session_id, tool_calls, dispatch, turn_opts),
}
}
fn __dispatch_tool_calls_with_middleware(tool_calls, tools, options) {
// Middleware-enabled path: dispatch sequentially so each call observes
// the audit-log/consent/redaction order in source order. Authors that
// want concurrency can wrap the inner call themselves.
var results = []
for call in tool_calls {
results = results.push(__invoke_tool(call, tools, options))
}
return results
}
fn __sync_tool_search_state(opts, turn_opts) {
if turn_opts?._tool_search_client == nil {
return opts
}
return opts + {_tool_search_client: turn_opts._tool_search_client}
}
fn __dispatch_results_list(dispatch) {
if dispatch == nil {
return []
}
if type_of(dispatch) == "list" {
return dispatch
}
return dispatch?.results ?? []
}
fn __tool_result_ok(result) {
if result?.ok != nil {
return result.ok ? true : false
}
if result?.success != nil {
return result.success ? true : false
}
let status = result?.status ?? ""
return status == "ok" || status == "success"
}
fn __tool_result_name(result) {
return result?.tool_name ?? result?.name ?? ""
}
fn __tool_names_by_status(dispatch, want_ok) {
let results = __dispatch_results_list(dispatch)
var names = []
for result in results {
let name = __tool_result_name(result)
if name != "" && __tool_result_ok(result) == want_ok {
names = names.push(name)
}
}
return names
}
fn __merge_tool_names(existing, additions) {
var merged = existing ?? []
let values = additions ?? []
for name in values {
if name != "" && !contains(merged, name) {
merged = merged.push(name)
}
}
return merged
}
fn __merge_hook_dict(base, patch, label) {
if patch == nil {
return base
}
if type_of(patch) != "dict" {
throw "agent_loop: post_turn_callback `" + label + "` must be a dict"
}
return base + patch
}
fn __strip_internal_keys(patch) {
if patch == nil {
return patch
}
if type_of(patch) != "dict" {
return patch
}
var clean = {}
for key in patch.keys() {
if !starts_with(key, "_") {
clean = clean + {[key]: patch[key]}
}
}
return clean
}
fn __apply_post_turn_options(opts, outcome) {
var updated = opts
let next_patch = __strip_internal_keys(outcome?.next_options)
updated = __merge_hook_dict(updated, next_patch, "next_options")
let llm_patch = outcome?.llm_options
if llm_patch != nil {
if type_of(llm_patch) != "dict" {
throw "agent_loop: post_turn_callback `llm_options` must be a dict"
}
let base_llm_options = updated?.llm_options ?? {}
updated = updated + {llm_options: base_llm_options + llm_patch}
}
return updated
}
fn __next_text_only_count(tool_count, consecutive_text_only) {
if tool_count == 0 {
return consecutive_text_only + 1
}
return 0
}
fn __requirement_satisfied(requirement, successful) {
if type_of(requirement) == "list" {
for candidate in requirement {
if contains(successful, candidate) {
return true
}
}
return false
}
return contains(successful, requirement)
}
fn __requirement_label(requirement) {
if type_of(requirement) == "list" {
return join(requirement, "|")
}
return to_string(requirement)
}
fn __missing_required_successful_tools(result, opts) {
let required = opts?.require_successful_tools
if required == nil || len(required) == 0 {
return []
}
let successful = result?.tools?.successful ?? []
var missing = []
for requirement in required {
if !__requirement_satisfied(requirement, successful) {
missing = missing.push(__requirement_label(requirement))
}
}
return missing
}
fn __required_tools_friction_event(result, opts, missing) {
let session_id = to_string(result?.session_id ?? "")
var recurrence = ["missing_required_tools=" + to_string(len(missing))]
if opts?.persona != nil {
recurrence = recurrence.push("persona=" + to_string(opts.persona))
}
return {
kind: "tool_gap",
source: "agent_loop.require_successful_tools",
actor: opts?.persona,
run_id: if session_id == "" {
nil
} else {
session_id
},
redacted_summary: "agent_loop completed without invoking required tool(s): "
+ join(missing, ", "),
recurrence_hints: recurrence,
metadata: {
missing_required_tools: missing,
successful_tool_names: result?.tools?.successful ?? [],
iterations: result?.llm?.iterations ?? 0,
preset_kind: opts?._preset_kind,
},
}
}
fn __require_successful_tools_envelope(result, missing, iterations) {
return {
schema_version: 1,
kind: "missing_required_tools",
reason: "Required tool(s) did not succeed",
missing: missing,
successful_tool_names: result?.tools?.successful ?? [],
iterations: iterations,
}
}
fn __maybe_emit_required_tools_friction(result, opts, missing) {
let event = __required_tools_friction_event(result, opts, missing)
let _ = try {
friction_record(event)
}
let session_id = to_string(result?.session_id ?? "")
if session_id != "" {
let _ = try {
agent_emit_event(session_id, "require_successful_tools_violation", event)
}
}
}
fn __enforce_required_successful_tools(result, opts) {
let missing = __missing_required_successful_tools(result, opts)
if len(missing) == 0 {
return result
}
let iterations = result?.llm?.iterations ?? 0
let envelope = __require_successful_tools_envelope(result, missing, iterations)
__maybe_emit_required_tools_friction(result, opts, missing)
return result
+ {
status: "failed",
final_status: "failed",
stop_reason: "missing_required_tools",
missing_required_tools: missing,
error: "Required tools did not succeed: " + join(missing, ", "),
error_envelope: envelope,
}
}
fn __agent_loop_finalize_failed(session, iteration) {
try {
agent_session_finalize(
session.session_id,
{final_status: "failed", stop_reason: "error", iterations: iteration},
)
} catch (e) {
}
}
fn __requirement_missing(requirement, successful) {
if type_of(requirement) == "list" {
for candidate in requirement {
if contains(successful, candidate) {
return false
}
}
return true
}
return !contains(successful, requirement)
}
fn __required_tools_missing_list(opts, successful_tools_seen) {
let required = opts?.require_successful_tools
if required == nil || len(required) == 0 {
return []
}
var missing = []
for requirement in required {
if __requirement_missing(requirement, successful_tools_seen) {
let label = if type_of(requirement) == "list" {
join(requirement, "|")
} else {
to_string(requirement)
}
missing = missing.push(label)
}
}
return missing
}
fn __required_tools_feedback(missing) {
return "Required tool(s) have not succeeded yet: "
+ join(missing, ", ")
+ ". Continue working and call the missing required tool(s) before finalizing."
}
fn __build_loop_state(args) {
let current_limit = args.current_limit
let iteration = args.iteration
let remaining = if iteration >= current_limit {
0
} else {
current_limit - iteration
}
let missing = args.missing_required_tools
return {
iteration: iteration,
budget: {
current_limit: current_limit,
max: args.budget_max,
remaining: remaining,
extension_count: args.extensions_used,
},
turn: {
tool_call_count: args.turn_tool_count,
successful_tool_names: args.turn_successful,
rejected_tool_names: args.turn_rejected,
text_chars: args.turn_text_chars,
native_fallback_used: args.turn_native_fallback_used,
},
session: {
successful_tool_names: args.session_successful,
rejected_tool_names: args.session_rejected,
required_tools_satisfied: len(missing) == 0,
required_tools_missing: missing,
},
completion: {
proposed: args.completion_proposed,
vetoed: args.completion_vetoed,
verdict: args.completion_verdict,
feedback: args.completion_feedback,
},
progress: {changed: args.progress_changed, summary: args.progress_summary},
}
}
fn __default_loop_control(state) {
if state.budget.remaining > 0 {
return nil
}
if state.completion.vetoed {
return {action: "extend", reason: "completion gate vetoed"}
}
if !state.session.required_tools_satisfied {
return {action: "extend", reason: "required tools missing"}
}
if state.progress.changed && state.turn.tool_call_count > 0 {
return {action: "extend", reason: "recent turn made progress"}
}
return nil
}
fn __interpret_loop_command(command) {
if command == nil {
return {action: "none", by: 0, until: 0, reason: "", status: ""}
}
if type_of(command) != "dict" {
throw "agent_loop: loop_control must return nil or a dict; got " + type_of(command)
}
let action = command?.action ?? "none"
if action != "none" && action != "extend" && action != "stop" {
throw "agent_loop: loop_control action must be \"none\", \"extend\", or \"stop\"; got "
+ to_string(action)
}
let by = command?.by ?? 0
if type_of(by) != "int" {
throw "agent_loop: loop_control `by` must be an integer; got " + type_of(by)
}
let until = command?.until ?? 0
if type_of(until) != "int" {
throw "agent_loop: loop_control `until` must be an integer; got " + type_of(until)
}
return {action: action, by: by, until: until, reason: command?.reason ?? "", status: command?.status ?? ""}
}
fn __loop_control_invoke(opts, budget, state) {
let policy = opts?.loop_control
if policy != nil {
return __interpret_loop_command(policy(state))
}
if budget?.mode == "adaptive" {
return __interpret_loop_command(__default_loop_control(state))
}
return __interpret_loop_command(nil)
}
fn __apply_extension(command, budget, current_max) {
let cap = budget?.max ?? current_max
let target = if command.until > 0 {
command.until
} else {
let by = if command.by > 0 {
command.by
} else {
budget?.extend_by ?? 0
}
current_max + by
}
let bounded = if target > cap {
cap
} else {
target
}
if bounded <= current_max {
return {extended: false, new_limit: current_max, delta: 0}
}
return {extended: true, new_limit: bounded, delta: bounded - current_max}
}
fn __record_budget_decision(decisions, iteration, action, old_limit, new_limit, reason, status) {
return decisions
.push(
{
iteration: iteration,
action: action,
old_limit: old_limit,
new_limit: new_limit,
reason: reason,
status: status,
},
)
}
fn __progress_summary_text(completion_vetoed, tool_count, visible_text) {
if completion_vetoed {
return "completion gate vetoed"
}
if tool_count > 0 {
return "executed " + to_string(tool_count) + " tool call(s)"
}
if trim(visible_text) != "" {
return "produced visible text"
}
return "no progress signal"
}
fn __snapshot_loop_state(args) {
let verdict = args.verdict
let completion_vetoed = verdict != nil && verdict?.vetoed
let completion_verdict = if verdict == nil {
nil
} else {
verdict?.verdict
}
let completion_feedback = if verdict == nil {
nil
} else {
verdict?.feedback
}
let visible_text = args.visible_text
let tool_count = args.tool_count
let turn_successful = args.turn_successful
let progress_changed = tool_count > 0 || len(turn_successful) > 0
|| trim(visible_text) != ""
let progress_summary = __progress_summary_text(completion_vetoed, tool_count, visible_text)
return __build_loop_state(
{
iteration: args.iteration,
current_limit: args.current_limit,
budget_max: args.budget_max,
extensions_used: args.extensions_used,
turn_tool_count: tool_count,
turn_successful: turn_successful,
turn_rejected: args.turn_rejected,
turn_text_chars: len(visible_text),
turn_native_fallback_used: args.turn_native_fallback_used,
session_successful: args.session_successful,
session_rejected: args.session_rejected,
missing_required_tools: args.missing_required_tools,
completion_proposed: args.completion_proposed,
completion_vetoed: completion_vetoed,
completion_verdict: completion_verdict,
completion_feedback: completion_feedback,
progress_changed: progress_changed,
progress_summary: progress_summary,
},
)
}
fn __apply_loop_command(state) {
let command = state.command
let session_id = state.session_id
let iteration = state.iteration
let current_max = state.current_max
let budget = state.budget
if command.action == "stop" {
let stop_status = if command.status != "" {
command.status
} else {
"stopped"
}
let stop_reason = if command.reason != "" {
command.reason
} else {
"loop_control"
}
let decisions = __record_budget_decision(
state.decisions,
iteration,
"stop",
current_max,
current_max,
command.reason,
stop_status,
)
agent_emit_event(
session_id,
"loop_control_decision",
{
iteration: iteration,
action: "stop",
old_limit: current_max,
new_limit: current_max,
reason: command.reason,
status: stop_status,
},
)
return {
stop: true,
final_status: stop_status,
stop_reason: stop_reason,
current_max: current_max,
extensions_used: state.extensions_used,
decisions: decisions,
}
}
if command.action == "extend" {
let applied = __apply_extension(command, budget, current_max)
if applied.extended {
let old_limit = current_max
let new_limit = applied.new_limit
let extensions_used = state.extensions_used + 1
let decisions = __record_budget_decision(
state.decisions,
iteration,
"extend",
old_limit,
new_limit,
command.reason,
"",
)
agent_emit_event(
session_id,
"loop_control_decision",
{
iteration: iteration,
action: "extend",
old_limit: old_limit,
new_limit: new_limit,
reason: command.reason,
status: "",
},
)
return {
stop: false,
final_status: "",
stop_reason: nil,
current_max: new_limit,
extensions_used: extensions_used,
decisions: decisions,
}
}
}
return {
stop: false,
final_status: "",
stop_reason: nil,
current_max: current_max,
extensions_used: state.extensions_used,
decisions: state.decisions,
}
}
@complexity(allow)
fn __agent_loop_run(message, session, initial_opts) {
var opts = initial_opts
var iteration = 0
var session_finalized = false
// Publish the resolved provider/model so any `.harn.prompt` rendered
// during turn-system construction (loop contract, tool contract,
// skills, …) can branch on `llm.capabilities.*` without manual
// option threading. Skipped when provider is empty — the render
// sees `llm = nil` and the existing branch falls through.
let __llm_ctx_provider = to_string(initial_opts?.provider ?? "")
let __llm_ctx_model = to_string(initial_opts?.model ?? "")
let __llm_ctx_pushed = __push_llm_render_context(__llm_ctx_provider, __llm_ctx_model)
defer {
if __llm_ctx_pushed {
__pop_llm_render_context()
}
}
let run = try {
opts = agent_mcp_bootstrap_if_needed(session, opts)
var stop_reason = nil
var final_status = ""
var terminal_error = nil
var verify_attempts = 0
var done_judge_invocations = 0
var consecutive_text_only = 0
var fallback_index = 0
var successful_tools_seen = []
var rejected_tools_seen = []
var stall_state = __stall_initial_state()
var stall_enabled_seen = false
let max_verify_attempts = opts?.max_verify_attempts ?? 20
let budget = opts?.iteration_budget
?? {
mode: "fixed",
initial: opts?.max_iterations ?? 50,
max: opts?.max_iterations ?? 50,
extend_by: 0,
expose_decisions: false,
}
var current_max = budget.initial
var extensions_used = 0
var budget_decisions = []
var last_tool_count = 0
while iteration < current_max {
if agent_budget_pre_call_blocked(session, opts) {
final_status = "budget_exhausted"
break
}
let turn_index = iteration
agent_emit_event(session.session_id, "turn_start", {iteration: turn_index + 1})
try {
__host_drain_file_edits(session.session_id)
} catch (e) {
nil
}
var turn_opts = agent_skills_match(session, opts, turn_index)
turn_opts = agent_tool_search_inject_if_needed(turn_opts)
agent_autocompact_if_needed(session, turn_opts)
let pending = agent_session_drain_feedback(session.session_id)
for note in pending {
agent_session_inject_feedback(session.session_id, note.kind, note.content)
}
let turn_system = agent_build_turn_system(session, turn_opts, turn_index)
let turn_messages = agent_build_turn_messages(session, turn_opts, turn_index)
let llm_overrides = turn_opts?.llm_options ?? {}
let base_opts = turn_opts + llm_overrides
let llm_opts = base_opts
+ {
messages: turn_messages,
session_id: session.session_id,
tool_format: turn_opts.tool_format,
_turn_iteration: turn_index + 1,
}
let call = __invoke_llm(message, turn_system, llm_opts)
if !call.ok {
final_status = call.status
stop_reason = call?.stop_reason ?? call.status
terminal_error = call?.error
break
}
let llm_result = call.value
iteration = iteration + 1
let raw_text = llm_result?.text ?? ""
let parsed = agent_parse_tool_calls(raw_text, turn_opts?.tools)
let visible_text = __visible_text(parsed, raw_text)
let normalized = llm_result + {text: visible_text, visible_text: visible_text}
agent_session_record_assistant(session.session_id, normalized)
let fallback_outcome = __detect_native_fallback(
llm_result,
parsed,
turn_opts,
fallback_index,
session.session_id,
turn_index,
)
fallback_index = fallback_outcome.fallback_index
let tool_calls = if fallback_outcome.triggered {
fallback_outcome.calls
} else {
__resolve_tool_calls(llm_result, parsed)
}
let stall_judge_due = __done_judge_stall_due(turn_opts, done_judge_invocations, turn_index + 1)
let stall_observation = __stall_observe_tool_calls(
session.session_id,
tool_calls,
turn_index + 1,
turn_opts?.stall_diagnostics,
stall_state,
stall_judge_due,
)
stall_state = stall_observation.state
stall_enabled_seen = stall_enabled_seen || stall_observation.enabled
if stall_judge_due && stall_observation?.warning != nil {
let stall_verify_opts = turn_opts
+ {_done_judge_due: true, _done_judge_trigger: "stalled"}
let stall_verdict = agent_verify_or_continue(session, stall_verify_opts, "stalled", visible_text, turn_index + 1)
if stall_verdict?.done_judge_invoked ?? false {
done_judge_invocations = done_judge_invocations + 1
}
if stall_verdict.vetoed {
if stall_observation?.feedback_deferred ?? false {
stall_state = __stall_inject_feedback(
session.session_id,
stall_observation.warning,
stall_observation.config,
stall_state,
)
}
} else {
agent_session_record_usage(session.session_id, llm_result)
let tool_count = len(tool_calls)
agent_emit_event(
session.session_id,
"turn_end",
{iteration: turn_index + 1, turn_info: {tool_count: tool_count, text: visible_text}},
)
final_status = "done"
stop_reason = "stalled_done_judge"
break
}
}
let dispatched = __dispatch_tool_calls(
session.session_id,
tool_calls,
turn_opts + {_turn_iteration: turn_index + 1, _tool_caller: opts?._tool_caller},
)
let dispatch = dispatched.dispatch
opts = __sync_tool_search_state(opts, dispatched.turn_opts)
successful_tools_seen = __merge_tool_names(successful_tools_seen, __tool_names_by_status(dispatch, true))
rejected_tools_seen = __merge_tool_names(rejected_tools_seen, __tool_names_by_status(dispatch, false))
let totals = agent_session_record_usage(session.session_id, llm_result)
let tool_count = len(tool_calls)
agent_emit_event(
session.session_id,
"turn_end",
{iteration: turn_index + 1, turn_info: {tool_count: tool_count, text: visible_text}},
)
if agent_budget_post_call_blocked(totals, turn_opts) {
final_status = "budget_exhausted"
break
}
consecutive_text_only = __next_text_only_count(tool_count, consecutive_text_only)
last_tool_count = tool_count
let turn_successful = __tool_names_by_status(dispatch, true)
let turn_rejected = __tool_names_by_status(dispatch, false)
let turn_max_nudges = turn_opts?.max_nudges ?? 8
let turn_loop_until_done = turn_opts?.loop_until_done ?? false
if turn_loop_until_done && tool_count == 0 && consecutive_text_only > turn_max_nudges {
final_status = "stuck"
stop_reason = "max_nudges"
break
}
let missing_required_for_loop = __required_tools_missing_list(opts, successful_tools_seen)
let cadence_loop_state = __snapshot_loop_state(
{
iteration: iteration,
current_limit: current_max,
budget_max: budget.max,
extensions_used: extensions_used,
tool_count: tool_count,
turn_successful: turn_successful,
turn_rejected: turn_rejected,
visible_text: visible_text,
turn_native_fallback_used: fallback_outcome.triggered && fallback_outcome.accepted,
session_successful: successful_tools_seen,
session_rejected: rejected_tools_seen,
missing_required_tools: missing_required_for_loop,
completion_proposed: false,
verdict: nil,
},
)
let post_turn_opts = turn_opts
+ {
_session_successful_tools: successful_tools_seen,
_session_rejected_tools: rejected_tools_seen,
_done_judge_invocations: done_judge_invocations,
_done_judge_loop_state: cadence_loop_state,
}
let outcome = agent_compute_post_turn(
session,
normalized + {raw_text: raw_text, parsed_done_marker: parsed?.done_marker ?? ""},
dispatch,
post_turn_opts,
turn_index,
)
opts = __apply_post_turn_options(opts, outcome)
var should_continue = outcome.kind == "continue"
var verdict_record = nil
if should_continue {
if opts?.daemon && len(tool_calls) == 0 {
agent_daemon_step(session, opts, iteration)
}
} else {
if outcome.needs_verify {
if verify_attempts >= max_verify_attempts {
final_status = "verify_exhausted"
stop_reason = outcome.stop_reason
break
}
let verify_opts = turn_opts + {_done_judge_due: outcome?.done_judge_due ?? true}
let verdict = agent_verify_or_continue(session, verify_opts, outcome.stop_reason, llm_result.text, turn_index)
verdict_record = verdict
if verdict?.done_judge_invoked ?? false {
done_judge_invocations = done_judge_invocations + 1
}
if verdict.vetoed {
verify_attempts = verify_attempts + 1
should_continue = true
}
}
let missing_now = __required_tools_missing_list(opts, successful_tools_seen)
if !should_continue && len(missing_now) > 0 {
agent_session_inject_feedback(
session.session_id,
"required_tools",
__required_tools_feedback(missing_now),
)
should_continue = true
}
if !should_continue {
stop_reason = outcome.stop_reason
break
}
}
let loop_state = __snapshot_loop_state(
{
iteration: iteration,
current_limit: current_max,
budget_max: budget.max,
extensions_used: extensions_used,
tool_count: tool_count,
turn_successful: turn_successful,
turn_rejected: turn_rejected,
visible_text: visible_text,
turn_native_fallback_used: fallback_outcome.triggered && fallback_outcome.accepted,
session_successful: successful_tools_seen,
session_rejected: rejected_tools_seen,
missing_required_tools: missing_required_for_loop,
completion_proposed: outcome.kind == "break",
verdict: verdict_record,
},
)
let command = __loop_control_invoke(opts, budget, loop_state)
let applied = __apply_loop_command(
{
command: command,
session_id: session.session_id,
iteration: iteration,
current_max: current_max,
extensions_used: extensions_used,
decisions: budget_decisions,
budget: budget,
},
)
current_max = applied.current_max
extensions_used = applied.extensions_used
budget_decisions = applied.decisions
if applied.stop {
final_status = applied.final_status
stop_reason = applied.stop_reason
break
}
}
if final_status == "" && iteration >= current_max && stop_reason == nil {
final_status = "budget_exhausted"
}
if opts?.daemon && final_status != "" {
agent_daemon_snapshot(session, opts, final_status, iteration)
}
try {
__host_drain_file_edits(session.session_id)
} catch (e) {
nil
}
let result = agent_session_finalize(
session.session_id,
{
final_status: final_status,
stop_reason: stop_reason ?? "",
iterations: iteration,
error: terminal_error,
},
)
session_finalized = true
let result_with_stalls = __apply_stall_result(result, stall_enabled_seen, stall_state)
let enforced = __enforce_required_successful_tools(result_with_stalls, opts)
if budget.expose_decisions {
enforced
+ {
adaptive_budget: {
mode: budget.mode,
initial: budget.initial,
max: budget.max,
final_limit: current_max,
extensions_used: extensions_used,
decisions: budget_decisions,
},
}
} else {
enforced
}
}
if is_err(run) {
if !session_finalized {
__agent_loop_finalize_failed(session, iteration)
}
throw unwrap_err(run)
}
return unwrap(run)
}
/** agent_loop. */
pub fn agent_loop(message, system_prompt = nil, options = nil) {
var opts = agent_loop_options(agent_progress_apply_options(options))
if system_prompt != nil && system_prompt != "" {
opts = opts + {system: system_prompt}
}
let session = agent_session_init(message, system_prompt, opts)
if session?.done {
return session.result
}
defer {
try {
__host_mcp_disconnect(session.session_id)
} catch (e) {
}
}
return __agent_loop_run(message, session, opts)
}