import {
agent_monologue_actuation_config,
agent_monologue_actuation_initial_state,
agent_monologue_actuation_observe,
} from "std/agent/monologue_actuation"
import "std/agent/stall_config"
import {
AgentStallConfig,
AgentStallObservation,
AgentStallState,
AgentStallWarning,
} from "std/agent/stall_types"
import { __agent_stall_fold_verify_state } from "std/agent/stall_verification"
import {
agent_emit_event,
agent_session_inject_feedback,
agent_session_totals,
} from "std/agent/state"
import { verification_gate_input } from "std/verification"
/**
* Reset only the action-stream tracking (signatures, observation/error
* streaks, ping-pong). Leaves the consecutive-trip escalation and feedback
* counters alone. Used when an action is exempt or the turn made no tool call.
*
* @effects: [agent]
* @errors: []
*/
pub fn __agent_stall_reset_action(state: AgentStallState) -> AgentStallState {
return state
+ {
last_signature: "",
streak: 0,
last_observation_signature: "",
last_outcome_kind: "",
same_observation_streak: 0,
same_error_streak: 0,
context_window_error_streak: 0,
ping_pong_a: "",
ping_pong_b: "",
ping_pong_alternations: 0,
}
}
pub fn __agent_tool_call_name(call) -> string {
return to_string(call?.name ?? call?.tool_name ?? "")
}
pub fn __agent_tool_call_args(call) -> dict {
const raw = call?.arguments ?? call?.tool_args
if type_of(raw) == "dict" {
return raw
}
return {}
}
pub fn __agent_tool_call_signature(call) -> string {
const args_text = json_stringify(__agent_tool_call_args(call))
return __agent_tool_call_name(call) + "\n" + args_text
}
/**
* observation classification (the polling-exemption mechanism)
*
* @effects: [agent]
* @errors: []
*/
pub fn __agent_stall_result_ok(result) -> bool {
if result?.ok != nil {
return result.ok ? true : false
}
if result?.success != nil {
return result.success ? true : false
}
const status = to_string(result?.status ?? "")
return status == "ok" || status == "success"
}
pub fn __agent_stall_result_name(result) -> string {
return to_string(result?.tool_name ?? result?.name ?? "")
}
/**
* A context-window / token-limit error is its own trip condition because
* repeating it is never productive and never recovers on its own.
*
* @effects: [agent]
* @errors: []
*/
pub fn __agent_stall_is_context_window_error(text: string) -> bool {
const lowered = lowercase(text)
return contains(lowered, "context window")
|| contains(lowered, "context length")
|| contains(lowered, "context_length")
|| contains(lowered, "maximum context")
|| contains(lowered, "token limit")
|| contains(lowered, "too many tokens")
|| contains(lowered, "context_length_exceeded")
|| contains(lowered, "max_tokens")
}
pub fn __agent_stall_delta_signature(current: dict) -> string {
const signatures = current?.signatures ?? []
const count = to_int(current?.count) ?? len(signatures)
return "err:" + sha256(json_stringify({signatures: signatures, count: count}))
}
pub fn __agent_stall_result_error_signature(delta, result) -> string {
if len(delta.current.signatures) > 0 {
return __agent_stall_delta_signature(delta.current)
}
const fallback = to_string(result?.error ?? result?.message ?? result?.result ?? "")
return "err:" + sha256(fallback)
}
pub fn __agent_stall_current_hashes(result) {
if type_of(result) != "dict" {
return nil
}
return result?.current_hashes
?? result?.currentHashes
?? result?.file_hashes
?? result?.fileHashes
}
pub fn __agent_stall_gate_input(previous, result) {
return verification_gate_input(previous, result, __agent_stall_current_hashes(result))
}
/**
* Outcome of a dispatched result, normalized for the detector: one of
* "ok" / "error" / "advisory" / "context_window", plus a byte-stable
* signature of the observation payload (for "ok") or diagnostic set.
*
* @effects: [agent]
* @errors: []
*/
pub fn __agent_stall_result_outcome(result) -> dict {
if __agent_stall_result_ok(result) {
const payload = json_stringify(result?.result ?? result?.output ?? result?.content)
return {kind: "ok", signature: "ok:" + sha256(payload)}
}
const error_text = to_string(result?.error ?? result?.message ?? result?.result ?? "")
if __agent_stall_is_context_window_error(error_text) {
return {kind: "context_window", signature: "ctx"}
}
const gate = __agent_stall_gate_input(nil, result)
if !gate.feedsGates {
return {kind: "advisory", signature: "advisory:" + sha256(json_stringify(gate.current))}
}
return {kind: "error", signature: __agent_stall_result_error_signature(gate.delta, result)}
}
/**
* Short, human-readable evidence snippet for a failing result (used to ground
* the strategy-shift nudge and the terminal current-failure payload). Never
* hashed — the byte-stable signature stays in __agent_stall_result_outcome.
*
* @effects: [agent]
* @errors: []
*/
pub fn __agent_stall_result_snippet(result) -> string {
if __agent_stall_result_ok(result) {
return ""
}
const text = to_string(result?.error ?? result?.message ?? result?.result ?? "")
const normalized = trim(text)
return __agent_clip_head_tail(normalized, 4000)
}
/**
* Clip an evidence string to `cap` chars WITHOUT amputating the tail. The root
* cause of a failure is often split across both ends — the exception/class up
* top, the decisive line (failing assertion, last compiler error) at the
* bottom. A blind head-only clip silently hid that tail from the model. Keep a
* generous head + tail with an explicit elision marker so nothing is dropped
* invisibly. `cap` is intentionally large; we are NOT trying to save tokens,
* only to bound a pathological multi-megabyte blob.
*
* @effects: [agent]
* @errors: []
*/
pub fn __agent_clip_head_tail(text, cap) {
const total = len(text)
if total <= cap {
return text
}
const tail_len = cap / 3
const head_len = cap - tail_len
const omitted = total - head_len - tail_len
const head = text.substring(0, head_len)
const tail = text.substring(total - tail_len, total)
return head + "\n…[" + to_string(omitted) + " chars elided — middle of output]…\n" + tail
}
/**
* Pick the representative verification result for a turn from its dispatch:
* the FIRST failing result (the diagnostic the agent must fix) if any,
* otherwise the first result (a "pass" turn). Returns nil when there is no
* dispatch result to classify (the turn made no observed tool call), so the
* caller leaves the current-failure model untouched.
*
* @effects: [agent]
* @errors: []
*/
pub fn __agent_stall_turn_result(prev_dispatch) {
if prev_dispatch == nil {
return nil
}
const results = if type_of(prev_dispatch) == "list" {
prev_dispatch
} else {
prev_dispatch?.results ?? []
}
let first = nil
for result in results {
if !__agent_stall_result_ok(result) {
return result
}
if first == nil {
first = result
}
}
return first
}
pub fn __agent_stall_delta_previous(state: AgentStallState) {
if state.last_diagnostic_class != "fail" {
return nil
}
const current = state.last_diagnostic_delta_current ?? {}
const signatures = current?.signatures ?? []
if len(signatures) == 0 {
return nil
}
return {
diagnostics: signatures,
diagnostic_count: to_int(current?.count) ?? len(signatures),
feedsGates: current?.feedsGates ?? current?.feeds_gates ?? true,
classification: {status: "bound_fresh", feedsGates: true, advisory: false},
}
}
pub fn __agent_stall_gate_failure(gate) -> bool {
return gate != nil && (gate?.gate_bearing_failure ?? false)
}
/**
* __agent_stall_fold_diagnostic folds the current-failure model for one turn.
* It is a PURE function of (state, prev_dispatch, turn_made_edit):
*
* - It classifies the turn's representative verification result (the first
* failing dispatch result, else the first result) through
* std/verification's diagnostic-delta helper. That helper owns
* location/temp-path normalization and the stale/advisory gate contract.
* - On a successful workspace-mutating edit this turn it bumps `write_epoch`
* (which arms the post-edit re-verify mandate the loop honors).
* - The failure SIGNATURE is the progress signal: a productive edit changes
* the error (signature changes => streak resets — legitimate progress, no
* false positive); a flailing edit leaves the SAME error (signature
* identical => streak advances — real thrash). So the streak advances on the
* same signature REGARDLESS of intervening edits.
* - It advances/resets `same_diagnostic_streak`:
* same failure signature => futile (streak + 1), even across edits
* different signature => reset to 1 (a different mistake, or progress)
* "pass" => clear streak, reverify_owed, edit_since_failure
*
* Result handling:
* - a FAILING result => fold the streak as above; this is verification
* evidence, so any owed re-verify is satisfied (clear reverify_owed /
* edit_since_failure). A turn that BOTH edits and re-tests still classifies
* here: __agent_stall_turn_result returns the FIRST failing result, so the
* re-test's failure is seen past the edit's own "ok" (the edit bumps the
* epoch; the fail still advances the streak).
* - a non-failing result, NOT an edit turn => a genuine passing verification
* clears the current-failure model.
* - a non-failing result on an EDIT turn => the result is the edit's own
* success, NOT verification of the failure. Owe a re-verify and preserve the
* failure model (same as the no-result edit case).
* - no result (prev_dispatch nil/empty) => if this turn made an edit on top of
* a live failure, owe a re-verify (reverify_owed / edit_since_failure) and
* bump the epoch; otherwise leave the model untouched (epoch still bumps on
* an edit).
*
* @effects: [agent]
* @errors: []
*/
pub fn __agent_stall_fold_diagnostic(
state: AgentStallState,
prev_dispatch,
turn_made_edit: bool,
) -> AgentStallState {
// Edit bookkeeping first: a successful workspace mutation advances the epoch,
// which arms the post-edit re-verify mandate in the loop.
const new_write_epoch = if turn_made_edit {
state.write_epoch + 1
} else {
state.write_epoch
}
// Elapsed-turns floor (#burin-cutover seam 5): count EVERY folded turn since the
// last clean verification. Every branch below carries this +1 forward EXCEPT the
// clean-pass branch, which resets it to 0. Always maintained (cheap, and never
// surfaced in any warning/result projection); the floor is consulted only when a
// `turns_since_clean_verify` knob is set, so this leaves default decisions
// byte-identical.
const new_turns_since_clean_verify = state.turns_since_clean_verify + 1
const result = __agent_stall_turn_result(prev_dispatch)
const result_is_non_ok = result != nil && !__agent_stall_result_ok(result)
const gate = if result_is_non_ok {
__agent_stall_gate_input(__agent_stall_delta_previous(state), result)
} else {
nil
}
const delta = gate?.delta
const result_is_gate_failure = __agent_stall_gate_failure(gate)
if !result_is_gate_failure {
// No FAILING verification this turn. A successful edit on top of a live
// failure is NOT proof the failure is resolved (its "ok" is the edit
// succeeding, not the check passing): owe a re-verify and preserve the
// current-failure model so the next re-fail of the SAME diagnostic is
// recognized across the edit. A genuine passing result on a NON-edit turn
// is real verification evidence and clears the model.
if result_is_non_ok && (delta == nil || delta.status != "cleared") {
if turn_made_edit && state.last_diagnostic_class == "fail" {
return state
+ {
write_epoch: new_write_epoch,
edit_since_failure: true,
reverify_owed: true,
turns_since_clean_verify: new_turns_since_clean_verify,
}
}
return state
+ {
write_epoch: new_write_epoch,
turns_since_clean_verify: new_turns_since_clean_verify,
}
}
if turn_made_edit {
if state.last_diagnostic_class == "fail" {
return state
+ {
write_epoch: new_write_epoch,
edit_since_failure: true,
reverify_owed: true,
turns_since_clean_verify: new_turns_since_clean_verify,
}
}
return state
+ {
write_epoch: new_write_epoch,
turns_since_clean_verify: new_turns_since_clean_verify,
}
}
if result == nil {
// No verification evidence and no edit-on-failure: leave the model alone.
return state
+ {
write_epoch: new_write_epoch,
turns_since_clean_verify: new_turns_since_clean_verify,
}
}
// A passing verification clears the current-failure model.
const pass_signature = if delta != nil {
"ok:" + sha256(json_stringify(delta.current))
} else {
__agent_stall_result_outcome(result).signature
}
const verified_epoch = if state.write_epoch > state.verified_write_epoch {
state.write_epoch
} else {
state.verified_write_epoch
}
return state
+ {
last_diagnostic_class: "pass",
last_diagnostic_signature: pass_signature,
last_diagnostic_snippet: "",
last_diagnostic_delta_current: {},
last_diagnostic_delta_status: delta?.status ?? "cleared",
last_diagnostic_delta_reason: delta?.reason ?? "current_result_is_passing",
write_epoch: new_write_epoch,
same_diagnostic_streak: 0,
failing_verify_turns: 0,
turns_since_clean_verify: 0,
edit_since_failure: false,
reverify_owed: false,
verified_write_epoch: verified_epoch,
}
}
// Gate-bearing failure: fold against the prior failure model. The diagnostic
// delta status is the progress signal, so the streak advances only when the
// std/verification helper says the normalized signature set is unchanged.
// Advisory/stale/unbound diagnostics never reach this branch.
const signature = __agent_stall_delta_signature(delta.current)
const same_signature = delta.status == "unchanged" && state.last_diagnostic_class == "fail"
const new_streak = if same_signature {
state.same_diagnostic_streak + 1
} else {
1
}
const failing_turns = state.failing_verify_turns + 1
// Total (non-consecutive) recurrence per signature (#burin-cutover seam 1). The
// consecutive `same_diagnostic_streak` above RESETS whenever an off-signature
// failure interposes, so it misses interleaved churn: a dead API / wrong error
// re-proposed N times with different failures churning between recurrences
// never builds a consecutive streak yet is provably stuck. This per-signature
// TOTAL counter never resets on a signature change, so the verify-state axis
// can trip a hard-recurrence cut on total occurrences regardless of ordering.
// Always maintained (cheap); consulted only when a progress_signal is supplied.
const sig_counts = state.verify_signature_counts ?? {}
const sig_total = (to_int(sig_counts[signature] ?? 0) ?? 0) + 1
// This is a verification result, so the post-edit re-verify mandate (if any)
// has now been satisfied: clear reverify_owed and the edit-since marker. The
// NEXT edit after this failure re-arms them.
return state
+ {
last_diagnostic_class: "fail",
last_diagnostic_signature: signature,
last_diagnostic_snippet: __agent_stall_result_snippet(result),
last_diagnostic_delta_current: delta.current,
last_diagnostic_delta_status: delta.status,
last_diagnostic_delta_reason: delta.reason,
write_epoch: new_write_epoch,
same_diagnostic_streak: new_streak,
failing_verify_turns: failing_turns,
turns_since_clean_verify: new_turns_since_clean_verify,
edit_since_failure: false,
reverify_owed: false,
verify_signature_counts: sig_counts + {[signature]: sig_total},
}
}
/**
* Find the dispatch result for the given tool name. The detector tracks one
* action at a time, so the first matching result is the relevant observation.
*
* @effects: [agent]
* @errors: []
*/
pub fn __agent_stall_outcome_for(prev_dispatch, tool_name: string) {
if prev_dispatch == nil {
return nil
}
const results = if type_of(prev_dispatch) == "list" {
prev_dispatch
} else {
prev_dispatch?.results ?? []
}
for result in results {
if __agent_stall_result_name(result) == tool_name {
return __agent_stall_result_outcome(result)
}
}
return nil
}
pub fn __agent_stall_feedback_text(warning: AgentStallWarning) -> string {
const pattern = warning.pattern
const count = to_string(warning.count)
// Evidence-aware repair loop: a strategy-shift nudge grounded in the actual
// diagnostic, not a blind "you repeated yourself" counter. This fires when
// the same failure has survived `stuck_same_diagnostic_after` repair turns
// with no intervening successful edit (or with edits that did not change the
// failure), so the right move is to change approach, not to retry harder.
if pattern == "stuck_same_diagnostic" {
const snippet = to_string(warning.diagnostic_snippet ?? "")
const evidence = if snippet != "" {
" The failing evidence is unchanged:\n" + snippet
} else {
""
}
return "Repair diagnostic: the same failure has persisted across "
+ count
+ " repair turns without changing."
+ evidence
+ "\nStop retrying the same fix. Re-read the relevant code, form a NEW hypothesis "
+ "about the root cause, or gather different evidence before editing again. If you "
+ "cannot make progress, report this specific blocker instead of repeating."
}
// Delivered-fix trigger (#burin-cutover seam 3): a repair WAS delivered for
// this exact failure and it STILL did not land — a stronger stuck signal than
// an untouched same-failure repeat, so the nudge is blunter.
if pattern == "delivered_fix_not_landing" {
const snippet = to_string(warning.diagnostic_snippet ?? "")
const evidence = if snippet != "" {
" The failing evidence is unchanged:\n" + snippet
} else {
""
}
return "Repair diagnostic: a fix was delivered for this failure but it did NOT land — the same failure has recurred across "
+ count
+ " turns despite the attempted remediation."
+ evidence
+ "\nThe current approach is not working. Do NOT re-apply the same fix. Re-read the"
+ " root cause from scratch, form a materially different hypothesis, or report this"
+ " specific blocker instead of repeating."
}
// Per-turn action-signal intake (#burin-cutover seam 4): the host flagged THIS
// turn as a flailing action (desperation write / truncated read / oscillating
// edit) — a strong single-turn stuck signal, so the nudge is direct.
if pattern == "flailing_action" {
return "Loop detector: this turn performed a flailing action (a desperation write, a"
+ " truncated/partial read, or an oscillating edit) — a strong single-turn signal that"
+ " the run is stuck. Stop repeating the current approach. Re-read the relevant code,"
+ " form a NEW hypothesis about the root cause, or report the specific blocker instead"
+ " of acting reflexively."
}
const core = if pattern == "repeated_same_observation" {
"the action `" + warning.tool_name + "` produced the same observation " + count
+ " times in a row"
} else if pattern == "repeated_error" {
"the action `" + warning.tool_name + "` failed with the same error " + count + " times in a row"
} else if pattern == "no_progress_monologue" {
count + " consecutive assistant messages made no progress (no tool call)"
} else if pattern == "errored_no_tool_call" {
"your previous turn ended with a provider error and emitted no tool call ("
+ count
+ " in a row) — re-emit the intended tool call"
} else if pattern == "no_net_progress_hard_cap" {
count
+ " failing verification turns occurred without a clean passing verify"
} else if pattern == "turns_since_clean_verify_floor" {
count
+ " turns elapsed since the last clean verification without a passing verify"
} else if pattern == "ping_pong" {
"the loop is ping-ponging between two actions (" + count + " cycles)"
} else if pattern
== "repeated_context_window_error" {
"the run hit a context-window error " + count + " times in a row"
} else {
"the last " + count + " tool calls repeated `" + warning.tool_name
+ "` with identical arguments"
}
if warning.hard_stop {
return "Loop detector (hard stop): "
+ core
+ ". "
+ to_string(warning.consecutive_trips)
+ " trips with no recovery — stop and report the blocker instead of repeating."
}
return "Stall diagnostic: "
+ core
+ ". Use different evidence, finish, or explain why repeating is necessary before doing so again."
}
pub fn __agent_stall_warning_record(
iteration: int,
tool_name: string,
args: dict,
signature: string,
pattern: string,
count: int,
consecutive_trips: int,
hard_stop: bool,
config: AgentStallConfig,
threshold: int,
) -> AgentStallWarning {
const args_text = json_stringify(args)
let record = {
iteration: iteration,
tool_name: tool_name,
repeat_count: count,
threshold: threshold,
arguments_digest: "sha256:" + sha256(args_text),
signature_digest: "sha256:" + sha256(signature),
pattern: pattern,
signature: signature,
count: count,
consecutive_trips: consecutive_trips,
hard_stop: hard_stop,
}
if config.include_arguments {
record = record + {arguments: args}
}
return record
}
/**
* agent_stall_inject_feedback injects bounded stall feedback.
*
* @effects: [agent]
* @errors: []
* @api_stability: experimental
*/
pub fn agent_stall_inject_feedback(
session_id: string,
warning: AgentStallWarning,
config: AgentStallConfig,
state: AgentStallState,
) -> AgentStallState {
const wants_feedback = config.inject_feedback ?? true
const should_feedback = wants_feedback && state.feedback_count < config.max_feedback
if should_feedback {
agent_session_inject_feedback(
session_id,
"stall_diagnostics",
__agent_stall_feedback_text(warning),
)
return state + {feedback_count: state.feedback_count + 1}
}
return state
}
pub fn __agent_stall_maybe_emit(
session_id: string,
warning: AgentStallWarning,
config: AgentStallConfig,
state: AgentStallState,
defer_feedback: bool,
) {
agent_emit_event(session_id, "agent_loop_stall_warning", warning)
if defer_feedback {
return {state: state, warning: warning, feedback_deferred: true}
}
return {
state: agent_stall_inject_feedback(session_id, warning, config, state),
warning: warning,
feedback_deferred: false,
}
}
/**
* ping-pong tracking: track the two action signatures in a candidate A/B
* alternation and the count of alternations. Two alternations (A->B->A) is one
* cycle. Returns the updated state; the cycle count lives in
* `ping_pong_alternations`.
*
* @effects: [agent]
* @errors: []
*/
pub fn __agent_stall_update_ping_pong(
state: AgentStallState,
signature: string,
) -> AgentStallState {
const a = state.ping_pong_a
const b = state.ping_pong_b
if a == "" {
return state + {ping_pong_a: signature, ping_pong_b: "", ping_pong_alternations: 0}
}
if b == "" {
if a == signature {
// Same action twice — this is the same-action repeat path, not ping-pong.
return state + {ping_pong_alternations: 0}
}
return state + {ping_pong_b: signature, ping_pong_alternations: 1}
}
const expected_next = if state.ping_pong_alternations % 2 == 1 {
a
} else {
b
}
if expected_next == signature {
return state + {ping_pong_alternations: state.ping_pong_alternations + 1}
}
if a == signature || b == signature {
// Within the A/B pair but out of strict alternation: restart the pair
// anchored on the current value.
return state + {ping_pong_a: signature, ping_pong_b: "", ping_pong_alternations: 0}
}
// A third distinct action: not a two-action ping-pong.
return state + {ping_pong_a: signature, ping_pong_b: "", ping_pong_alternations: 0}
}
/**
* Apply a fired trip: bump the consecutive-trip escalation and decide whether
* to flag a hard stop. Returns the state with the trip recorded.
*
* @effects: [agent]
* @errors: []
*/
pub fn __agent_stall_register_trip(
state: AgentStallState,
config: AgentStallConfig,
) -> AgentStallState {
const trips = state.consecutive_trips + 1
return state + {consecutive_trips: trips, hard_stop: trips >= config.hard_stop_after_trips}
}
/**
* Apply a terminal hard-stop trip. Used for safety floors that should stop the
* run immediately rather than waiting for several warning/recovery cycles.
*
* @effects: [agent]
* @errors: []
*/
pub fn __agent_stall_register_immediate_hard_stop(state: AgentStallState) -> AgentStallState {
return state + {consecutive_trips: state.consecutive_trips + 1, hard_stop: true}
}
/**
* A non-tripping step recovers: reset the consecutive-trip escalation so the
* hard stop only fires on genuinely uninterrupted thrashing.
*
* @effects: [agent]
* @errors: []
*/
pub fn __agent_stall_register_recovery(state: AgentStallState) -> AgentStallState {
return state + {consecutive_trips: 0, hard_stop: false}
}
/**
* Build an ENABLED `AgentStallObservation` from an already-folded `state`.
* `hard_stop` always mirrors `state.hard_stop` (the trip/recovery helpers are
* the sole authority on it), so callers pass the post-fold state and never
* re-derive the flag. The `config.enabled == false` short-circuit is the one
* observation that is NOT built here — it returns the untouched incoming state
* with `enabled: false`.
*
* @effects: []
* @errors: []
*/
pub fn __agent_stall_observation(
state: AgentStallState,
warning,
config: AgentStallConfig,
feedback_deferred: bool,
) -> AgentStallObservation {
return {
state: state,
enabled: true,
warning: warning,
feedback_deferred: feedback_deferred,
config: config,
hard_stop: state.hard_stop,
}
}
/**
* Emit one diagnostic-trip warning and fold it into the observation. Shared by
* the three repair-overlay trips in `__agent_stall_apply_repair`
* (`no_net_progress_hard_cap`, `delivered_fix_not_landing`,
* `stuck_same_diagnostic`), which differ only in the already-`tripped` state,
* the pattern/count/threshold diagnostic fields, and whether the
* `remediation_delivered` marker is set.
*
* @effects: [agent]
* @errors: []
*/
pub fn __agent_stall_emit_diagnostic_trip(
session_id: string,
config: AgentStallConfig,
tripped: AgentStallState,
iteration: int,
pattern: string,
count: int,
threshold: int,
defer_feedback: bool,
remediation_delivered = false,
) -> AgentStallObservation {
let warning = __agent_stall_warning_record(
iteration,
"",
{},
tripped.last_diagnostic_signature,
pattern,
count,
tripped.consecutive_trips,
tripped.hard_stop,
config,
threshold,
)
+ {
diagnostic_class: tripped.last_diagnostic_class,
diagnostic_signature: tripped.last_diagnostic_signature,
diagnostic_snippet: tripped.last_diagnostic_snippet,
}
if remediation_delivered {
warning = warning + {remediation_delivered: true}
}
const emitted = __agent_stall_maybe_emit(session_id, warning, config, tripped, defer_feedback)
const next_state = emitted.state + {warnings: emitted.state.warnings.push(warning)}
return __agent_stall_observation(next_state, warning, config, emitted.feedback_deferred ?? false)
}
/**
* Detect a no-progress monologue turn (assistant produced text but no tool
* call). Returns the observation when the streak crosses the threshold.
*
* @effects: [agent]
* @errors: []
*/
pub fn __agent_stall_observe_no_progress(
session_id: string,
iteration: int,
config: AgentStallConfig,
state: AgentStallState,
defer_feedback: bool,
turn_stop_reason = "",
) -> AgentStallObservation {
// An agent message breaks any action-repeat / ping-pong run.
let next_state = __agent_stall_reset_action(state)
next_state = next_state + {no_progress_streak: next_state.no_progress_streak + 1}
if next_state.no_progress_streak < config.no_progress_messages {
next_state = __agent_stall_register_recovery(next_state)
return __agent_stall_observation(next_state, nil, config, false)
}
next_state = __agent_stall_register_trip(next_state, config)
// A turn that ended with a provider error (stop_reason=error) but emitted no
// tool call narrated its intent without landing an action — the cheap-model
// eval-meter failure class. Give it cause-specific feedback instead of the
// generic "made no progress" nag, so the model knows its turn errored and to
// re-emit the intended tool call.
const pattern = if to_string(turn_stop_reason) == "error" {
"errored_no_tool_call"
} else {
"no_progress_monologue"
}
const warning = __agent_stall_warning_record(
iteration,
"",
{},
"",
pattern,
next_state.no_progress_streak,
next_state.consecutive_trips,
next_state.hard_stop,
config,
config.no_progress_messages,
)
const emitted = __agent_stall_maybe_emit(session_id, warning, config, next_state, defer_feedback)
next_state = emitted.state + {warnings: emitted.state.warnings.push(warning)}
return __agent_stall_observation(next_state, warning, config, emitted.feedback_deferred ?? false)
}
/**
* __agent_stall_process_call folds a single tool call (and the observation it
* produced last turn) into the detector state and decides which condition, if
* any, tripped. Extracted from `agent_stall_observe_tool_calls` to keep that
* function within the cyclomatic-complexity budget. Returns the updated state
* plus a `trip` dict (or nil). Trip ordering: context-window, then repeated
* error, then repeated identical observation, then ping-pong, then the legacy
* signature-only threshold (used only when no dispatch results are available).
*
* @effects: [agent]
* @errors: []
*/
pub fn __agent_stall_process_call(
state: AgentStallState,
config: AgentStallConfig,
call,
tool_name: string,
prev_dispatch,
) -> dict {
const signature = __agent_tool_call_signature(call)
const same_action = signature == state.last_signature
const outcome = __agent_stall_outcome_for(prev_dispatch, tool_name)
// Observation signature of the action that just repeated. When dispatch
// results are unavailable, fall back to the action signature itself so the
// legacy adjacent-repeat behavior still trips.
const obs_signature = if outcome != nil {
outcome.signature
} else {
"sig:" + sha256(signature)
}
const outcome_kind = if outcome != nil {
outcome.kind
} else {
"ok"
}
// Ping-pong is independent of the same-action streak and only considers
// distinct alternating actions; update it first.
let next_state = __agent_stall_update_ping_pong(state, signature)
const streak = if same_action {
next_state.streak + 1
} else {
1
}
const repeated = if streak > 1 {
next_state.repeated_tool_calls + 1
} else {
next_state.repeated_tool_calls
}
// The polling exemption (principled, not an allowlist): a repeated identical
// action counts toward the same-observation condition UNLESS we have positive
// evidence the observation changed — i.e. both this turn's and last turn's
// observations are real (non-fallback) AND differ. A changing observation is
// legitimate polling (status advances, watched file mutates) and resets the
// streak. When observations are unavailable (no dispatch results threaded in)
// the streak counts the raw signature repeat, preserving the legacy
// adjacent-identical-call behavior so existing thresholds still trip.
const both_known = outcome != nil && next_state.last_outcome_kind != ""
const observation_changed = both_known && obs_signature != next_state.last_observation_signature
const new_same_observation_streak = if !same_action || outcome_kind != "ok" {
if outcome_kind == "ok" {
1
} else {
0
}
} else if observation_changed {
1
} else {
next_state.same_observation_streak + 1
}
// Same-error streak: same action producing the same (or unknown) error.
// Distinct errors reset, because the agent is making different mistakes.
const new_same_error_streak = if !same_action || outcome_kind != "error" {
if outcome_kind == "error" {
1
} else {
0
}
} else if observation_changed {
1
} else {
next_state.same_error_streak + 1
}
const new_context_streak = if outcome_kind == "context_window" {
next_state.context_window_error_streak + 1
} else {
0
}
next_state = next_state
+ {
last_signature: signature,
streak: streak,
repeated_tool_calls: repeated,
last_observation_signature: obs_signature,
last_outcome_kind: if outcome != nil {
outcome_kind
} else {
""
},
same_observation_streak: new_same_observation_streak,
same_error_streak: new_same_error_streak,
context_window_error_streak: new_context_streak,
}
const ping_pong_cycles = next_state.ping_pong_alternations / 2
const trip = __agent_stall_classify_trip(
config,
{
context_streak: new_context_streak,
same_error_streak: new_same_error_streak,
same_observation_streak: new_same_observation_streak,
outcome_kind: outcome_kind,
ping_pong_cycles: ping_pong_cycles,
outcome_present: outcome != nil,
streak: streak,
},
)
return {state: next_state, trip: trip}
}
/**
* Decide which stall condition, if any, tripped for a single processed call.
* Extracted from `__agent_stall_process_call` to keep both within the
* cyclomatic-complexity budget. Trip ordering: context-window, repeated error,
* repeated identical observation, ping-pong, then the legacy signature-only
* threshold. When the evidence-aware repair loop is enabled it OWNS the
* repeated-failure semantics (same diagnostic across repair turns) via the
* diagnostic-grounded `stuck_same_diagnostic` pattern in the repair overlay, so
* the legacy `repeated_error` / `repeated_same_observation` trips are suppressed
* here (no double-fire; the richer repair nudge wins). Context-window and
* ping-pong remain — they are orthogonal failure modes the repair model does
* not cover.
*
* @effects: [agent]
* @errors: []
*/
pub fn __agent_stall_classify_trip(config: AgentStallConfig, s) {
const repair_owns_repeat = config.repair_aware
if s.context_streak >= config.repeat_context_window_error {
return {
pattern: "repeated_context_window_error",
count: s.context_streak,
threshold: config.repeat_context_window_error,
}
}
if !repair_owns_repeat && s.same_error_streak >= config.repeat_same_error {
return {
pattern: "repeated_error",
count: s.same_error_streak,
threshold: config.repeat_same_error,
}
}
if !repair_owns_repeat
&& s.outcome_kind == "ok"
&& s.same_observation_streak >= config.repeat_same_observation {
return {
pattern: "repeated_same_observation",
count: s.same_observation_streak,
threshold: config.repeat_same_observation,
}
}
if s.ping_pong_cycles >= config.ping_pong_cycles {
return {pattern: "ping_pong", count: s.ping_pong_cycles, threshold: config.ping_pong_cycles}
}
if !s.outcome_present && s.streak >= config.threshold {
return {pattern: "repeated_same_signature", count: s.streak, threshold: config.threshold}
}
return nil
}
/**
* agent_stall_observe_tool_calls detects degenerate agent loops.
*
* Conditions: same action -> identical observation, same action -> same
* error, no-progress monologue, ping-pong, and repeated context-window
* errors. `prev_dispatch` carries the previous turn's dispatch results so the
* detector can attribute an observation signature to the repeated action;
* pass nil to fall back to the signature-only same-action heuristic. The
* polling exemption is principled: a repeated identical action counts toward
* the same-observation and ping-pong conditions only when its observation is
* byte-identical across repeats. `exempt_tools` remains a secondary escape
* hatch.
*
* @effects: [agent]
* @errors: [agent_loop]
* @api_stability: experimental
*/
pub fn __agent_stall_observe_core(
session_id: string,
tool_calls: list,
iteration: int,
raw_config,
state: AgentStallState,
defer_feedback: bool,
prev_dispatch = nil,
turn_text = "",
had_parse_errors = false,
turn_stop_reason = "",
) -> AgentStallObservation {
const config = __agent_stall_config(raw_config)
if !config.enabled {
return {
state: state,
enabled: false,
warning: nil,
feedback_deferred: false,
config: config,
hard_stop: false,
}
}
// A turn whose tool calls were all dropped by the parser carries real intent
// (it tried to act); the malformed-call path already injected purpose-built
// parse-guidance feedback. Treat it as a neutral recovery turn so it does NOT
// count toward the no-progress monologue streak — otherwise the loop nags the
// model to "emit one well-formed tool call" when it already did, just with a
// malformed body. Gated purely on the syntactic parse-error signal.
if had_parse_errors {
return __agent_stall_observation(
__agent_stall_register_recovery(__agent_stall_reset_action(state)),
nil,
config,
false,
)
}
// Soft progress-report tools (e.g. `agent_progress`) report status without
// advancing task state, so they are excluded from the real-action stream: a
// turn whose only call is such a report is treated as no-progress.
const substantive_calls = tool_calls.filter(
{ c -> !contains(SOFT_PROGRESS_TOOLS, __agent_tool_call_name(c)) },
)
if len(substantive_calls) == 0 {
// No substantive tool call this turn — either none at all, or only soft
// progress reports. With visible text or a progress report, that is a
// no-progress monologue candidate; a fully empty turn resets the action
// stream.
if to_string(turn_text) != "" || len(tool_calls) > 0 {
return __agent_stall_observe_no_progress(
session_id,
iteration,
config,
state,
defer_feedback,
turn_stop_reason,
)
}
return __agent_stall_observation(
__agent_stall_register_recovery(__agent_stall_reset_action(state)),
nil,
config,
false,
)
}
let next_state = state + {no_progress_streak: 0}
let emitted_warning = nil
let feedback_deferred = false
for call in substantive_calls {
const tool_name = __agent_tool_call_name(call)
if tool_name == "" || contains(config.exempt_tools, tool_name) {
next_state = __agent_stall_register_recovery(__agent_stall_reset_action(next_state))
continue
}
const processed = __agent_stall_process_call(next_state, config, call, tool_name, prev_dispatch)
next_state = processed.state
const trip = processed.trip
if trip != nil {
next_state = __agent_stall_register_trip(next_state, config)
const warning = __agent_stall_warning_record(
iteration,
tool_name,
__agent_tool_call_args(call),
__agent_tool_call_signature(call),
trip.pattern,
trip.count,
next_state.consecutive_trips,
next_state.hard_stop,
config,
trip.threshold,
)
const emitted = __agent_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)}
} else {
next_state = __agent_stall_register_recovery(next_state)
}
}
return __agent_stall_observation(next_state, emitted_warning, config, feedback_deferred)
}
/**
* Evidence-aware repair overlay (#repair-diagnostics). The current-failure
* fold is shared by post-edit verification and repair-aware nudges. When
* `post_edit_reverify` is enabled this folds the PRIOR turn's verification
* result + whether that turn made a successful edit (turn_made_edit), so the
* loop can require a final verify even when repair nudges are disabled. When
* `repair_aware` is also enabled this trips the "stuck_same_diagnostic"
* strategy-shift nudge when the same diagnostic has survived
* `stuck_same_diagnostic_after` repair turns.
* The trip rides the EXISTING `agent_loop_stall_warning` event +
* `__agent_stall_register_trip` escalation; no new event type is introduced.
* When the core observation already produced a warning this turn we keep it
* and only fold the model (we do not stack two warnings in one turn).
*/
/**
* Delivered-fix intake (#burin-cutover seam 3). The `remediation_delivered`
* callback may return either a bool (the ORIGINAL per-turn form, unchanged) or an
* int CUMULATIVE count of remediations delivered for the active failure signature
* (burin's `injections_now` fact — cumulative "since this signature became
* active"). For the int form harn derives the per-turn "a fix was delivered"
* event by diffing the returned count against the count last SEEN for that
* signature (`state.remediation_delivered_*`), resetting the baseline to 0 on a
* signature change. Returns `{delivered, signature, count}`; the caller folds
* `signature`/`count` back into state so the next turn's diff has the right
* baseline. A nil return means "no fix this turn" (delivered false, tracking
* unchanged), matching the pre-seam falsy-return behavior. Any other return type
* throws loudly.
*
* @effects: [agent]
* @errors: []
*/
pub fn __agent_stall_eval_remediation(state: AgentStallState, signature: string, raw) -> dict {
if type_of(raw) == "int" {
const same_sig = state.remediation_delivered_signature == signature
const baseline = if same_sig {
state.remediation_delivered_count
} else {
0
}
return {count: raw, delivered: raw > baseline, signature: signature}
}
if raw == nil {
return {
count: state.remediation_delivered_count,
delivered: false,
signature: state.remediation_delivered_signature,
}
}
if type_of(raw) == "bool" {
return {
count: state.remediation_delivered_count,
delivered: raw,
signature: state.remediation_delivered_signature,
}
}
throw "agent_loop: stall_diagnostics.remediation_delivered callback must return a bool, an int cumulative count, or nil; got "
+ type_of(
raw,
)
}
pub fn __agent_stall_apply_repair(
session_id: string,
config: AgentStallConfig,
observation: AgentStallObservation,
iteration: int,
prev_dispatch,
turn_made_edit: bool,
defer_feedback: bool,
) -> AgentStallObservation {
const should_fold_current_failure = config.post_edit_reverify || config.repair_aware
// The elapsed-turns clean-verify floor (#burin-cutover seam 5) is an INDEPENDENT
// axis: its counter is maintained inside `fold_diagnostic`, so the fold must run
// whenever the `turns_since_clean_verify` knob is set — even with neither repair
// mode on. Without this the knob validates at config time but the floor never
// trips (the counter stays 0), silently dead unless post_edit_reverify or
// repair_aware happens to also be set. Every downstream trip below is already
// independently gated on its own config, so folding here changes nothing when only
// the knob is set beyond maintaining the counter and arming its floor.
const clean_verify_floor_active = config.turns_since_clean_verify != nil
if !should_fold_current_failure && !clean_verify_floor_active {
return observation
}
const folded = __agent_stall_fold_diagnostic(observation.state, prev_dispatch, turn_made_edit)
const hard_cap_due = config.no_net_progress_extend_guard
&& folded.last_diagnostic_class == "fail"
&& folded.failing_verify_turns
>= config
.no_net_progress_hard_cap_after
&& observation.state.failing_verify_turns
< config
.no_net_progress_hard_cap_after
if hard_cap_due {
const tripped = __agent_stall_register_immediate_hard_stop(folded)
return __agent_stall_emit_diagnostic_trip(
session_id,
config,
tripped,
iteration,
"no_net_progress_hard_cap",
tripped.failing_verify_turns,
config.no_net_progress_hard_cap_after,
defer_feedback,
)
}
// Elapsed-turns hard floor (#burin-cutover seam 5, default OFF). Distinct from the
// failing-verify `no_net_progress_hard_cap_after` above: this counts EVERY folded
// turn since the last clean verify (edit/read storms included), so a slow-draining
// run that rarely produces a FAILING verification — never building the failing-verify
// counter — still stops. Fires ONCE on the crossing edge, forcing a terminal stuck
// stop. Inert unless the caller sets `turns_since_clean_verify` (nil = off), so today's
// behavior is byte-identical.
const clean_floor_due = config.turns_since_clean_verify != nil
&& folded.turns_since_clean_verify
>= config
.turns_since_clean_verify
&& observation.state.turns_since_clean_verify
< config
.turns_since_clean_verify
if clean_floor_due {
const tripped = __agent_stall_register_immediate_hard_stop(folded)
return __agent_stall_emit_diagnostic_trip(
session_id,
config,
tripped,
iteration,
"turns_since_clean_verify_floor",
tripped.turns_since_clean_verify,
config.turns_since_clean_verify ?? 0,
defer_feedback,
)
}
// Delivered-fix trigger (#burin-cutover seam 3, HIGHEST precedence). When the
// host supplies a `remediation_delivered` fact callback and reports that a
// repair WAS delivered for the ACTIVE failure signature yet that SAME failure
// is still landing, escalate one turn sooner than the plain-repeat stuck nudge
// (STALL_DELIVERED_FIX_AFTER = 2 < stuck_same_diagnostic_after = 3): a fix that
// was actually attempted and did not land is a stronger stuck signal than an
// untouched same-failure repeat. harn owns only the TRIGGER (the distinct
// `delivered_fix_not_landing` pattern + warning.remediation_delivered = true);
// the host's smart-escalation actuator decides what to do with it. The
// callback is invoked ONLY inside the guard below, so an absent callback (or a
// turn that does not otherwise qualify) leaves behavior byte-identical. Fires
// regardless of `repair_aware` (it keys on the fact, not the repair nudge).
const delivered_streak_advanced = folded.same_diagnostic_streak
> observation.state
.same_diagnostic_streak
const delivered_candidate = config.remediation_delivered != nil
&& folded.last_diagnostic_class
== "fail"
&& folded.same_diagnostic_streak >= STALL_DELIVERED_FIX_AFTER
&& delivered_streak_advanced
&& observation.warning == nil
// `current` carries the cumulative-count baseline forward from the delivered
// intake below (byte-identical to `folded` on the bool/absent path, since the
// tracking fields are then unchanged). Every downstream branch folds `current`
// so a non-tripping evaluation still records the new per-signature count.
let current = folded
if delivered_candidate {
const raw_delivered = config.remediation_delivered(
{
prev_dispatch: prev_dispatch,
session_id: session_id,
signature: folded.last_diagnostic_signature,
},
)
const evaluated = __agent_stall_eval_remediation(
folded,
folded.last_diagnostic_signature,
raw_delivered,
)
current = folded
+ {
remediation_delivered_count: evaluated.count,
remediation_delivered_signature: evaluated.signature,
}
if evaluated.delivered {
const tripped = __agent_stall_register_trip(current, config)
return __agent_stall_emit_diagnostic_trip(
session_id,
config,
tripped,
iteration,
"delivered_fix_not_landing",
tripped.same_diagnostic_streak,
STALL_DELIVERED_FIX_AFTER,
defer_feedback,
true,
)
}
}
if !config.repair_aware {
return observation + {state: current}
}
// Trip ONCE when the streak first crosses the threshold, so the strategy-shift
// nudge fires a single time per stuck episode rather than on every subsequent
// identical failure. Require that the streak ADVANCED this turn (a fresh
// same-signature failure was folded) — an edit/owe turn preserves the streak
// at the threshold without re-folding a failure, and must not re-trip. A later
// streak reset (a different/passing signature) re-arms the trip for the next
// genuinely-stuck episode. Also require that no other stall pattern already
// fired this turn (avoid double-nudging).
const streak_advanced = current.same_diagnostic_streak > observation.state.same_diagnostic_streak
const should_trip = current.same_diagnostic_streak == config.stuck_same_diagnostic_after
&& streak_advanced
&& observation.warning == nil
if !should_trip {
return observation + {state: current}
}
const tripped = __agent_stall_register_trip(current, config)
return __agent_stall_emit_diagnostic_trip(
session_id,
config,
tripped,
iteration,
"stuck_same_diagnostic",
tripped.same_diagnostic_streak,
config.stuck_same_diagnostic_after,
defer_feedback,
)
}
/**
* Per-turn action-signal intake (#burin-cutover seam 4, default OFF). When the
* host supplies an `action_signal` fact callback and it returns truthy for THIS
* turn's payload, a single flailing action (a desperation shell write / truncated
* read / edit-oscillation verdict) trips IMMEDIATELY — no streak needed — riding
* the EXISTING `agent_loop_stall_warning` event with the distinct `flailing_action`
* pattern. Consecutive flailing turns accumulate a DEDICATED `action_flail_trips`
* counter, escalating to a hard stop once it reaches `hard_stop_after_trips`. That
* counter is kept SEPARATE from `consecutive_trips` on purpose: the core resets
* `consecutive_trips` to 0 on every non-core-tripping substantive turn (an
* action_signal-only flailing stream never trips a core detector), so reusing it
* would cap the flail escalation at 1 and the hard stop would be unreachable. A
* non-flailing turn resets the flail streak to 0. Only ONE warning is emitted per
* turn: if the core/repair path already tripped, the action signal is skipped (no
* double-nudge) and the streak is left untouched. The callback is invoked ONLY when
* configured, and this returns the observation UNCHANGED when the callback is nil,
* so the default (callback unset) is byte-identical.
*
* @effects: [agent]
* @errors: []
*/
pub fn __agent_stall_apply_action_signal(
session_id: string,
config: AgentStallConfig,
observation: AgentStallObservation,
iteration: int,
payload,
defer_feedback: bool,
) -> AgentStallObservation {
if config.action_signal == nil {
return observation
}
if observation.warning != nil {
// A core/repair detector already owns this turn's single warning; the
// action-flail streak is a distinct axis and is left untouched.
return observation
}
const flailing = config.action_signal(payload)
if !flailing {
// A non-flailing substantive turn breaks the consecutive-flail streak.
if observation.state.action_flail_trips == 0 {
return observation
}
return observation + {state: observation.state + {action_flail_trips: 0}}
}
// Dedicated accumulator, decoupled from `consecutive_trips` (which the core resets
// every non-core-tripping turn): repeated flailing escalates to a hard stop at
// `hard_stop_after_trips`. The flail count rides the warning's `count` field
// (passed below); we deliberately do NOT write it into `consecutive_trips`, so the
// core escalation axis stays uncontaminated — otherwise a later core trip at a
// flail->core-trip boundary would escalate one step early.
const trips = observation.state.action_flail_trips + 1
const hard_stop = trips >= config.hard_stop_after_trips
const tripped = observation.state + {action_flail_trips: trips, hard_stop: hard_stop}
return __agent_stall_emit_diagnostic_trip(
session_id,
config,
tripped,
iteration,
"flailing_action",
trips,
config.hard_stop_after_trips,
defer_feedback,
)
}
pub fn __agent_stall_observe_monologue_actuation(
observation: AgentStallObservation,
) -> AgentStallObservation {
return observation
+ {
state: observation.state
+ {
monologue_actuation: agent_monologue_actuation_observe(
observation.state.monologue_actuation,
observation.config.monologue_actuation,
to_string(observation.warning?.pattern ?? ""),
),
},
}
}
/**
* Fold one turn through stall, repair, action, verification, and optional
* actuation detectors. `turn_made_edit` describes the prior dispatch;
* `payload` reaches configured host fact callbacks only. Completed-dispatch
* actuation credit is owned by the loop after tool results exist.
*
* @effects: [agent]
* @errors: [agent_loop]
* @api_stability: experimental
*/
pub fn agent_stall_observe_tool_calls(
session_id: string,
tool_calls: list,
iteration: int,
raw_config,
state: AgentStallState,
defer_feedback: bool,
prev_dispatch = nil,
turn_text = "",
had_parse_errors = false,
turn_made_edit = false,
turn_stop_reason = "",
payload = nil,
) -> AgentStallObservation {
const observation = __agent_stall_observe_core(
session_id,
tool_calls,
iteration,
raw_config,
state,
defer_feedback,
prev_dispatch,
turn_text,
had_parse_errors,
turn_stop_reason,
)
if !observation.enabled {
return observation
}
const repaired = __agent_stall_apply_repair(
session_id,
observation.config,
observation,
iteration,
prev_dispatch,
turn_made_edit,
defer_feedback,
)
// Per-turn action-signal intake (#burin-cutover seam 4). Inert (returns `repaired`
// unchanged) unless an `action_signal` callback is configured, so the default is
// byte-identical. Applied after the repair overlay so it respects the one-warning-
// per-turn discipline.
const signaled = __agent_stall_apply_action_signal(
session_id,
observation.config,
repaired,
iteration,
payload,
defer_feedback,
)
let resolved = signaled
if observation.config.progress_signal != nil {
resolved = resolved
+ {
state: __agent_stall_fold_verify_state(
resolved.state,
observation.config,
payload,
turn_made_edit,
),
}
}
return __agent_stall_observe_monologue_actuation(resolved)
}