import {
CommandHoldState,
CommandWaitOptions,
command_ledger_has_awaited,
command_ledger_hold,
command_ledger_release,
command_ledger_should_hold,
} from "std/agent/command_ledger"
import {
AgentLoopBudget,
AgentLoopBudgetDecision,
agent_loop_apply_command,
agent_loop_control_invoke,
agent_loop_snapshot_state,
} from "std/agent/control"
import { agent_daemon_step } from "std/agent/daemon"
import { agent_evaluate_completion } from "std/agent/judge"
import { AgentToolDispatch, __tool_names_by_status } from "std/agent/loop_result_status"
import { __agent_loop_with_llm_render_context } from "std/agent/loop_support"
import { __inject_feedback_with_tool_repair } from "std/agent/loop_tool_calls"
import {
__agent_loop_post_edit_reverify_mandated,
__agent_loop_state_budget_fields,
__apply_post_turn_options,
__completion_judge_hit,
__completion_stop_reason,
__done_judge_hit,
__loop_progress_nudge_text,
__next_progress_count,
__next_text_only_count,
__sync_tool_search_state,
__turn_made_edit,
agent_stage,
} from "std/agent/loop_turn_options"
import { agent_compute_post_turn } from "std/agent/postturn"
import { agent_tool_call_paradigm } from "std/agent/preflight"
import {
agent_required_tools_inject_feedback,
agent_required_tools_missing_from_session,
} from "std/agent/required_tools"
import { agent_scratchpad_reorganize_if_due } from "std/agent/scratchpad"
import { AgentStallState } from "std/agent/stall_types"
import {
agent_stall_no_net_progress,
agent_stall_repair_config,
agent_stall_repeated_verified_pass,
} from "std/agent/stall_verification"
import { agent_stance_post_turn } from "std/agent/stance"
import { agent_emit_event, agent_session_inject_feedback } from "std/agent/state"
pub type AgentLoopPostTurnAction = "break" | "continue"
pub type AgentLoopCommandHoldAction = "continue" | "proceed"
pub type AgentLoopCommandHoldInput = {
ledger: list,
command_wait: CommandWaitOptions,
hold_state: CommandHoldState,
reentry_cap: int?,
iteration: int,
tool_count: int,
}
pub type AgentLoopCommandHoldResult = {
action: AgentLoopCommandHoldAction,
ledger: list,
hold_state: CommandHoldState,
reentry_cap: int?,
reset_progress: bool,
}
pub type AgentLoopPostTurnContext = {
budget: AgentLoopBudget,
bridge_step_delivered: int,
completed_no_information_gain: bool,
consecutive_failure_count: int,
dispatch: AgentToolDispatch,
dispatched_turn_opts: dict,
fallback_accepted: bool,
fallback_triggered: bool,
had_parse_errors: bool,
iteration: int,
iteration_index: int,
llm_result: dict,
loop_start_ms: int,
normalized: dict,
parsed_done_marker: string,
raw_text: string,
rejected_tools_seen: list,
successful_tools_seen: list,
tool_calls: list,
totals: dict,
turn_llm_opts: dict,
turn_opts: dict,
visible_text: string,
}
pub type AgentLoopPostTurnState = {
budget_decisions: list<AgentLoopBudgetDecision>,
consecutive_text_only: int,
current_max: int,
done_judge_invocations: int,
done_judge_vetoes: int,
extensions_used: int,
feedback_history: dict,
final_status: string,
last_tool_count: int,
made_source_write: bool,
opts: dict,
stall_state: AgentStallState,
stop_reason: string?,
terminal_write_unverified: bool,
turns_since_progress: int,
verify_attempts: int,
verify_completion_judge_invocations: int,
verify_completion_judge_vetoes: int,
}
pub type AgentLoopPostTurnResult = {action: AgentLoopPostTurnAction, state: AgentLoopPostTurnState}
/**
* Arbitrate a no-tool turn while the loop owns awaited command handles.
* The result names the outer-loop action instead of leaking continue semantics
* across the module seam.
*
* @effects: [agent, host]
* @errors: []
*/
pub fn agent_loop_command_hold_transition(
harness: Harness,
session_id: string,
input: AgentLoopCommandHoldInput,
) -> AgentLoopCommandHoldResult {
// Command hold. A no-tool-call segment while an `awaited` handle is
// live does not end the turn: the loop parks on the inbox (zero
// inference) and re-enters the model only on its own sparse,
// delta-gated schedule with one coalesced `command_status` digest. A
// done attempt with pending awaited leases lands here too
// (`tool_count == 0`), so it cannot silently finish over a running
// build — it gets a forced-resolution re-entry.
if input.tool_count != 0 || !command_ledger_should_hold(input.ledger, input.tool_count) {
return {
action: "proceed",
ledger: input.ledger,
hold_state: input.hold_state,
reentry_cap: input.reentry_cap,
reset_progress: false,
}
}
const hold = command_ledger_hold(
harness,
session_id,
input.ledger,
input.command_wait,
input.hold_state,
)
let ledger = hold.ledger
const hold_state = hold.hold_state
let reentry_cap = input.reentry_cap
agent_stage(harness.agent, session_id, "command_hold", {iteration: input.iteration})
agent_emit_event(
harness.agent,
session_id,
"typed_checkpoint",
{
schema: "harn.command_hold.v1",
kind: "command_hold",
iteration: input.iteration,
outcome: hold.outcome,
reentries: hold_state.reentries,
awaited: command_ledger_has_awaited(ledger),
},
)
if hold.outcome == "digest" {
// A snapshot of which background commands were running or finished at this
// iteration. It is re-digested on every post-turn that still holds, so a
// durable copy only preserves a stale roster beside the current one.
agent_session_inject_feedback(harness.agent, session_id, "command_status", hold.digest, 1)
reentry_cap = hold.reentry_cap
// Only a fully retired, green terminal digest may arbitrate same-turn DONE.
if !(hold.terminal_succeeded && !command_ledger_has_awaited(ledger)) {
return {
action: "continue",
ledger: ledger,
hold_state: hold_state,
reentry_cap: reentry_cap,
reset_progress: true,
}
}
}
if hold.outcome == "interrupted" {
// A non-command inbox entry (user interrupt / peer message) woke
// the inclusive park; re-enter so the normal `iteration_start`
// drains deliver it.
return {
action: "continue",
ledger: ledger,
hold_state: hold_state,
reentry_cap: reentry_cap,
reset_progress: false,
}
}
if hold.outcome == "exhausted" {
// Re-entry budget spent: signal `await_exhausted` into the stuck-
// detector adapter (recovery, not a kill) and flip the awaited rows
// to service leases so the hold cannot busy-loop — the processes
// keep running until the session-end reaper.
for row in ledger {
if row?.lease == "awaited" {
ledger = command_ledger_release(ledger, row.handle_id)
}
}
agent_session_inject_feedback(
harness.agent,
session_id,
"await_exhausted",
"Background commands are still running after repeated waits. Take a different action now: "
+ "act on what you have, kill_command to abort, or release_command to leave them running.",
1,
)
return {
action: "continue",
ledger: ledger,
hold_state: hold_state,
reentry_cap: reentry_cap,
reset_progress: true,
}
}
// outcome == "resolved": every awaited handle is terminal / killed /
// released and its completion was already surfaced; fall through to
// the loop's normal done resolution.
return {
action: "proceed",
ledger: ledger,
hold_state: hold_state,
reentry_cap: reentry_cap,
reset_progress: false,
}
}
// "Take a different action now" is advice about the immediate next turn.
// The awaited rows are released just above, so the condition this
// describes is already gone by the turn after the one that reads it.
/**
* Fold post-turn completion, verification, progress, and budget-control policy
* into one explicit transition. The caller owns only applying the returned
* state and honoring its named outer-loop action.
*/
@complexity(allow)
pub fn agent_loop_post_turn_transition(
harness: Harness,
session: dict,
context: AgentLoopPostTurnContext,
state: AgentLoopPostTurnState,
max_verify_attempts: int,
) -> AgentLoopPostTurnResult {
let next = state
const tool_count = len(context.tool_calls)
next = next
+ {
consecutive_text_only: __next_text_only_count(tool_count, next.consecutive_text_only),
last_tool_count: tool_count,
}
const turn_successful = __tool_names_by_status(context.dispatch, true)
const turn_rejected = __tool_names_by_status(context.dispatch, false)
const made_progress = len(turn_successful) > 0
next = next
+ {turns_since_progress: __next_progress_count(made_progress, next.turns_since_progress)}
const turn_max_nudges = context.turn_opts?.max_nudges ?? 8
const turn_loop_until_done = context.turn_opts?.loop_until_done ?? false
const text_only_nudge_budget_exceeded = turn_loop_until_done
&& tool_count == 0
&& next.consecutive_text_only > turn_max_nudges
const missing_required_for_loop = agent_required_tools_missing_from_session(
next.opts,
context.successful_tools_seen,
)
const cadence_loop_state = agent_loop_snapshot_state(
{
iteration: context.iteration,
current_limit: next.current_max,
budget_max: context.budget.max,
extensions_used: next.extensions_used,
tool_count: tool_count,
turn_successful: turn_successful,
turn_rejected: turn_rejected,
visible_text: context.visible_text,
turn_native_fallback_used: context.fallback_triggered && context.fallback_accepted,
session_successful: context.successful_tools_seen,
session_rejected: context.rejected_tools_seen,
missing_required_tools: missing_required_for_loop,
completion_proposed: false,
verdict: nil,
}
+ __agent_loop_state_budget_fields(
harness.agent,
harness.clock,
session.session_id,
context.totals,
context.loop_start_ms,
context.budget,
context.consecutive_failure_count,
),
)
const post_turn_opts = __sync_tool_search_state(context.turn_opts, context.dispatched_turn_opts)
+ {
_session_successful_tools: context.successful_tools_seen,
_session_rejected_tools: context.rejected_tools_seen,
_consecutive_text_only: next.consecutive_text_only,
_done_judge_invocations: next.done_judge_invocations,
_done_judge_loop_state: cadence_loop_state,
_turn_tool_call_feedback: (context.fallback_triggered && !context.fallback_accepted)
|| context.had_parse_errors,
}
const outcome = __agent_loop_with_llm_render_context(
harness,
context.turn_llm_opts,
{ render_harness, _effective ->
agent_compute_post_turn(
render_harness,
session,
context.normalized
+ {raw_text: context.raw_text, parsed_done_marker: context.parsed_done_marker},
context.dispatch,
post_turn_opts,
context.iteration_index,
)
},
)
let updated_opts = __apply_post_turn_options(next.opts, outcome)
updated_opts = agent_stance_post_turn(harness.agent, harness.runtime, session, updated_opts)
next = next + {opts: updated_opts}
const repair_cfg = agent_stall_repair_config(context.turn_opts?.stall_diagnostics)
const reverify_mandated = __agent_loop_post_edit_reverify_mandated(
repair_cfg,
context.turn_opts,
next.stall_state,
context.dispatch,
next.opts?.tools,
)
if __turn_made_edit(context.dispatch, next.opts?.tools) {
next = next + {terminal_write_unverified: true, made_source_write: true}
}
let should_continue = outcome.kind == "continue" || context.bridge_step_delivered > 0
let verdict_record = nil
let post_edit_reverify_confirmed = false
if should_continue && reverify_mandated && next.verify_attempts < max_verify_attempts {
const reverdict = agent_evaluate_completion(
harness,
session,
context.turn_opts
+ {
_done_judge_due: false,
_done_judge_invocations: next.done_judge_invocations,
_verify_completion_judge_invocations: next.verify_completion_judge_invocations,
_feedback_history: next.feedback_history,
},
"post_edit_reverify",
context.llm_result.text,
context.iteration_index,
context.raw_text,
)
verdict_record = reverdict
next = next
+ {
feedback_history: reverdict.feedback_history ?? next.feedback_history,
verify_completion_judge_invocations: next.verify_completion_judge_invocations
+ __completion_judge_hit(reverdict),
}
if reverdict.action == "stop_unverified" {
return {
action: "break",
state: next
+ {
final_status: "completion_unverified",
stop_reason: __completion_stop_reason(reverdict),
},
}
} else if reverdict.action == "continue" {
next = next
+ {
verify_attempts: next.verify_attempts + 1,
verify_completion_judge_vetoes: next.verify_completion_judge_vetoes
+ __completion_judge_hit(reverdict),
}
} else {
next = next
+ {stall_state: next.stall_state + {reverify_owed: false}, terminal_write_unverified: false}
post_edit_reverify_confirmed = true
}
}
if should_continue {
if next.opts?.daemon && tool_count == 0 {
agent_daemon_step(harness.agent, harness.fs, session, next.opts, context.iteration)
}
} else {
if outcome.needs_verify || reverify_mandated {
if next.verify_attempts >= max_verify_attempts {
return {
action: "break",
state: next
+ {final_status: "verify_exhausted", stop_reason: outcome.stop_reason},
}
}
const verdict = agent_evaluate_completion(
harness,
session,
context.turn_opts
+ {
_done_judge_due: outcome?.done_judge_due ?? true,
_done_judge_invocations: next.done_judge_invocations,
_verify_completion_judge_invocations: next.verify_completion_judge_invocations,
_feedback_history: next.feedback_history,
},
outcome.stop_reason,
context.llm_result.text,
context.iteration_index,
context.raw_text,
)
verdict_record = verdict
next = next
+ {
feedback_history: verdict.feedback_history ?? next.feedback_history,
done_judge_invocations: next.done_judge_invocations + __done_judge_hit(verdict),
verify_completion_judge_invocations: next.verify_completion_judge_invocations
+ __completion_judge_hit(verdict),
}
if verdict.action == "stop_unverified" {
return {
action: "break",
state: next
+ {
final_status: "completion_unverified",
stop_reason: __completion_stop_reason(verdict),
},
}
} else if verdict.action == "continue" {
should_continue = true
next = next
+ {
verify_attempts: next.verify_attempts + 1,
done_judge_vetoes: next.done_judge_vetoes + __done_judge_hit(verdict),
verify_completion_judge_vetoes: next.verify_completion_judge_vetoes
+ __completion_judge_hit(verdict),
}
} else {
next = next
+ {
stall_state: next.stall_state + {reverify_owed: false},
terminal_write_unverified: false,
}
if reverify_mandated {
post_edit_reverify_confirmed = true
}
}
}
const missing_now = agent_required_tools_missing_from_session(
next.opts,
context.successful_tools_seen,
)
if !should_continue && len(missing_now) > 0 {
agent_required_tools_inject_feedback(harness.agent, session.session_id, missing_now)
should_continue = true
}
if !should_continue {
return {action: "break", state: next + {stop_reason: outcome.stop_reason}}
}
}
if should_continue && post_edit_reverify_confirmed && context.iteration >= next.current_max {
const missing_after_reverify = agent_required_tools_missing_from_session(
next.opts,
context.successful_tools_seen,
)
if len(missing_after_reverify) == 0 {
return {
action: "break",
state: next + {final_status: "done", stop_reason: "post_edit_reverify"},
}
}
}
if should_continue {
const repeated_verified_pass = agent_stall_repeated_verified_pass(
context.turn_opts?.stall_diagnostics,
next.stall_state,
)
if repeated_verified_pass && len(turn_rejected) == 0 && trim(context.visible_text) == "" {
const missing_after_verified_pass = agent_required_tools_missing_from_session(
next.opts,
context.successful_tools_seen,
)
if len(missing_after_verified_pass) == 0 {
return {
action: "break",
state: next + {final_status: "done", stop_reason: "repeated_verified_pass"},
}
}
}
if text_only_nudge_budget_exceeded {
return {action: "break", state: next + {final_status: "stuck", stop_reason: "max_nudges"}}
}
agent_scratchpad_reorganize_if_due(
harness.agent,
harness.llm,
session,
next.opts,
context.iteration_index,
{
reason: "iteration_end",
outcome_kind: outcome.kind,
bridge_step_delivered: context.bridge_step_delivered,
tool_count: tool_count,
},
)
if next.turns_since_progress >= 2
&& next.turns_since_progress <= turn_max_nudges
&& !(outcome?.nudged_this_turn ?? false)
&& len(turn_rejected) == 0 {
const has_tools = len(next.opts?.tools?.tools ?? []) > 0
const paradigm = agent_tool_call_paradigm(harness.llm, context.turn_opts)
const made_tool_calls = tool_count > 0
const nudge_text = __loop_progress_nudge_text(
next.turns_since_progress,
has_tools,
paradigm,
made_tool_calls,
)
__inject_feedback_with_tool_repair(
harness.agent,
session.session_id,
"no_progress_streak",
nudge_text,
)
agent_emit_event(
harness.agent,
session.session_id,
"no_progress_streak_nudge",
{
iteration: context.iteration_index,
content: nudge_text,
streak: next.turns_since_progress,
turns_since_progress: next.turns_since_progress,
has_tools: has_tools,
made_tool_calls: made_tool_calls,
},
)
}
}
const loop_no_net_progress = agent_stall_no_net_progress(
context.turn_opts?.stall_diagnostics,
next.stall_state,
)
|| agent_stall_repeated_verified_pass(context.turn_opts?.stall_diagnostics, next.stall_state)
const loop_state = agent_loop_snapshot_state(
{
iteration: context.iteration,
current_limit: next.current_max,
budget_max: context.budget.max,
extensions_used: next.extensions_used,
tool_count: tool_count,
turn_successful: turn_successful,
turn_rejected: turn_rejected,
visible_text: context.visible_text,
turn_native_fallback_used: context.fallback_triggered && context.fallback_accepted,
session_successful: context.successful_tools_seen,
session_rejected: context.rejected_tools_seen,
missing_required_tools: missing_required_for_loop,
completion_proposed: outcome.kind == "break",
verdict: verdict_record,
progress_no_net_advance: loop_no_net_progress,
progress_no_information_gain: context.completed_no_information_gain,
}
+ __agent_loop_state_budget_fields(
harness.agent,
harness.clock,
session.session_id,
context.totals,
context.loop_start_ms,
context.budget,
context.consecutive_failure_count,
),
)
const command = agent_loop_control_invoke(next.opts, context.budget, loop_state)
const applied = agent_loop_apply_command(
harness.agent,
{
command: command,
session_id: session.session_id,
iteration: context.iteration,
current_max: next.current_max,
extensions_used: next.extensions_used,
decisions: next.budget_decisions,
budget: context.budget,
},
)
next = next
+ {
current_max: applied.current_max,
extensions_used: applied.extensions_used,
budget_decisions: applied.decisions,
}
if applied.stop {
return {
action: "break",
state: next + {final_status: applied.final_status, stop_reason: applied.stop_reason},
}
}
return {action: "continue", state: next}
}