// @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 { 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"
fn __invoke_llm(message, turn_system, llm_opts) {
let result = try {
llm_call(message, turn_system, llm_opts)
}
if !is_err(result) {
return {ok: true, value: unwrap(result)}
}
let err = unwrap_err(result)
let reason = if type_of(err) == "dict" {
err?.reason ?? ""
} else {
""
}
if reason == "budget_exceeded" {
return {ok: false, status: "budget_exhausted"}
}
throw err
}
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 __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 dispatch = agent_dispatch_tool_batch(
tool_calls,
turn_opts?.tools,
{
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,
},
)
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 __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 __apply_post_turn_options(opts, outcome) {
var updated = opts
updated = __merge_hook_dict(updated, outcome?.next_options, "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 __enforce_required_successful_tools(result, opts) {
let missing = __missing_required_successful_tools(result, opts)
if len(missing) == 0 {
return result
}
return result
+ {
status: "failed",
final_status: "failed",
stop_reason: "missing_required_tools",
missing_required_tools: missing,
error: "Required tools did not succeed: " + join(missing, ", "),
}
}
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
let run = try {
opts = agent_mcp_bootstrap_if_needed(session, opts)
var stop_reason = nil
var final_status = ""
var verify_attempts = 0
var consecutive_text_only = 0
var fallback_index = 0
var successful_tools_seen = []
var rejected_tools_seen = []
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})
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}
let call = __invoke_llm(message, turn_system, llm_opts)
if !call.ok {
final_status = call.status
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 dispatched = __dispatch_tool_calls(session.session_id, tool_calls, turn_opts)
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 post_turn_opts = turn_opts
+ {_session_successful_tools: successful_tools_seen, _session_rejected_tools: rejected_tools_seen}
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 verdict = agent_verify_or_continue(session, turn_opts, outcome.stop_reason, llm_result.text, turn_index)
verdict_record = verdict
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 missing_required_for_loop = __required_tools_missing_list(opts, successful_tools_seen)
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)
}
let result = agent_session_finalize(
session.session_id,
{final_status: final_status, stop_reason: stop_reason ?? "", iterations: iteration},
)
session_finalized = true
let enforced = __enforce_required_successful_tools(result, 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(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)
}