// @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
}
if result?.status != "done" {
return result + {missing_required_tools: 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, ", "),
}
}
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 __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 max_iterations = opts?.max_iterations ?? 50
while iteration < max_iterations {
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)
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)
if outcome.kind == "continue" {
if opts?.daemon && len(tool_calls) == 0 {
agent_daemon_step(session, opts, iteration)
}
continue
}
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)
if verdict.vetoed {
verify_attempts = verify_attempts + 1
continue
}
}
stop_reason = outcome.stop_reason
break
}
if final_status == "" && iteration >= max_iterations && 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
__enforce_required_successful_tools(result, opts)
}
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)
}