import { agent_feedback_emit } from "std/agent/feedback"
import {
agent_monologue_actuation_config,
agent_monologue_actuation_initial_state,
agent_monologue_actuation_observe,
} from "std/agent/monologue_actuation"
import {
__agent_stall_delta_signature,
__agent_stall_gate_input,
__agent_stall_process_call,
__agent_stall_result_ok,
__agent_stall_result_outcome,
__agent_tool_call_args,
__agent_tool_call_name,
__agent_tool_call_signature,
} from "std/agent/stall_action_observation"
import "std/agent/stall_config"
import { __agent_stall_feedback_text } from "std/agent/stall_feedback"
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_totals } from "std/agent/state"
/**
* 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_success_streak: 0,
same_error_streak: 0,
context_window_error_streak: 0,
ping_pong_a: "",
ping_pong_b: "",
ping_pong_alternations: 0,
}
}
/**
* 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(
agent: HarnessAgent,
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, __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(agent, 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},
}
}
pub type AgentStallWarningInput = {
iteration: int,
tool_name: string,
args: dict,
signature: string,
pattern: string,
count: int,
consecutive_trips: int,
hard_stop: bool,
threshold: int,
}
pub fn __agent_stall_warning_record(
input: AgentStallWarningInput,
config: AgentStallConfig,
) -> AgentStallWarning {
const args_text = json_stringify(input.args)
let record = {
iteration: input.iteration,
tool_name: input.tool_name,
repeat_count: input.count,
threshold: input.threshold,
arguments_digest: "sha256:" + sha256(args_text),
signature_digest: "sha256:" + sha256(input.signature),
pattern: input.pattern,
signature: input.signature,
count: input.count,
consecutive_trips: input.consecutive_trips,
hard_stop: input.hard_stop,
}
if config.include_arguments {
record = record + {arguments: input.args}
}
return record
}
/**
* agent_stall_inject_feedback injects bounded stall feedback.
*
* @effects: [agent]
* @errors: []
* @api_stability: experimental
*/
pub fn agent_stall_inject_feedback(
agent: HarnessAgent,
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_feedback_emit(
agent,
session_id,
[
{
kind: "stall_diagnostics",
content: __agent_stall_feedback_text(warning),
source: "stall_detector",
reason: warning.pattern,
action: if warning.hard_stop {
"stop"
} else {
"continue"
},
},
],
)
return state + {feedback_count: state.feedback_count + 1}
}
return state
}
pub fn __agent_stall_maybe_emit(
agent: HarnessAgent,
session_id: string,
warning: AgentStallWarning,
config: AgentStallConfig,
state: AgentStallState,
defer_feedback: bool,
) {
agent_emit_event(agent, session_id, "agent_loop_stall_warning", warning)
if defer_feedback {
return {state: state, warning: warning, feedback_deferred: true}
}
return {
state: agent_stall_inject_feedback(agent, session_id, warning, config, state),
warning: warning,
feedback_deferred: false,
}
}
/**
* 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(
agent: HarnessAgent,
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: iteration,
tool_name: "",
args: {},
signature: tripped.last_diagnostic_signature,
pattern: pattern,
count: count,
consecutive_trips: tripped.consecutive_trips,
hard_stop: tripped.hard_stop,
threshold: threshold,
},
config,
)
+ {
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(
agent,
session_id,
warning,
config,
tripped,
defer_feedback,
)
const next_state = emitted.state + {warnings: emitted.state.warnings.appending(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(
agent: HarnessAgent,
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: iteration,
tool_name: "",
args: {},
signature: "",
pattern: pattern,
count: next_state.no_progress_streak,
consecutive_trips: next_state.consecutive_trips,
hard_stop: next_state.hard_stop,
threshold: config.no_progress_messages,
},
config,
)
const emitted = __agent_stall_maybe_emit(
agent,
session_id,
warning,
config,
next_state,
defer_feedback,
)
next_state = emitted.state + {warnings: emitted.state.warnings.appending(warning)}
return __agent_stall_observation(next_state, warning, config, emitted.feedback_deferred ?? false)
}
/**
* Fold repeated unparseable turns through the shared bounded recovery ladder.
* @effects: [agent]
* @errors: []
*/
pub fn __agent_stall_observe_parse_rejection(
agent: HarnessAgent,
session_id: string,
iteration: int,
config: AgentStallConfig,
state: AgentStallState,
defer_feedback: bool,
) -> AgentStallObservation {
let next_state = __agent_stall_reset_action(state)
next_state = next_state + {parse_rejection_streak: next_state.parse_rejection_streak + 1}
if next_state.parse_rejection_streak < config.repeat_parse_rejection {
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)
const warning = __agent_stall_warning_record(
{
iteration: iteration,
tool_name: "",
args: {},
signature: "parse_rejection",
pattern: "repeated_parse_rejection",
count: next_state.parse_rejection_streak,
consecutive_trips: next_state.consecutive_trips,
hard_stop: next_state.hard_stop,
threshold: config.repeat_parse_rejection,
},
config,
)
const emitted = __agent_stall_maybe_emit(
agent,
session_id,
warning,
config,
next_state,
defer_feedback,
)
next_state = emitted.state + {warnings: emitted.state.warnings.appending(warning)}
return __agent_stall_observation(next_state, warning, config, emitted.feedback_deferred ?? false)
}
/**
* agent_stall_observe_tool_calls detects degenerate agent loops.
*
* Conditions: identical observations for one action (adjacent or recurring
* across interleaved calls), 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 action; pass nil to fall back to the
* signature-only adjacent-action heuristic. Changing observations are
* structurally exempt because their result signatures use distinct recurrence
* counters. `exempt_tools` remains a secondary escape hatch.
*
* @effects: [agent]
* @errors: [agent_loop]
* @api_stability: experimental
*/
pub fn __agent_stall_observe_core(
agent: HarnessAgent,
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 = "",
turn_made_edit = false,
) -> 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,
}
}
// Parse rejection is an observed failure state in its own right. Keep it out
// of the generic no-progress monologue detector, but feed it through the same
// bounded warning/feedback/hard-stop ladder so recovery does not depend on a
// successfully parsed call.
if had_parse_errors {
return __agent_stall_observe_parse_rejection(
agent,
session_id,
iteration,
config,
state,
defer_feedback,
)
}
const active_state = state + {parse_rejection_streak: 0}
// 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(
agent,
session_id,
iteration,
config,
active_state,
defer_feedback,
turn_stop_reason,
)
}
return __agent_stall_observation(
__agent_stall_register_recovery(__agent_stall_reset_action(active_state)),
nil,
config,
false,
)
}
let next_state = active_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(
agent,
next_state,
config,
call,
tool_name,
prev_dispatch,
turn_made_edit,
)
next_state = processed.state
const trip = processed.trip
if trip != nil {
next_state = if trip?.advisory_only ?? false {
// A cumulative observation recurrence is evidence worth surfacing, but
// never participates in the hard-stop ladder. Interposed activity may
// be legitimate even when one dead observation keeps recurring.
__agent_stall_register_recovery(next_state)
} else {
__agent_stall_register_trip(next_state, config)
}
const warning = __agent_stall_warning_record(
{
iteration: iteration,
tool_name: tool_name,
args: __agent_tool_call_args(call),
signature: __agent_tool_call_signature(call),
pattern: trip.pattern,
count: trip.count,
consecutive_trips: next_state.consecutive_trips,
hard_stop: next_state.hard_stop,
threshold: trip.threshold,
},
config,
)
const emitted = __agent_stall_maybe_emit(
agent,
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.appending(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(
agent: HarnessAgent,
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(
agent,
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(
agent,
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(
agent,
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(
agent,
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(
agent,
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(
agent: HarnessAgent,
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(
agent,
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(
agent: HarnessAgent,
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(
agent,
session_id,
tool_calls,
iteration,
raw_config,
state,
defer_feedback,
prev_dispatch,
turn_text,
had_parse_errors,
turn_stop_reason,
turn_made_edit,
)
if !observation.enabled {
return observation
}
const repaired = __agent_stall_apply_repair(
agent,
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(
agent,
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)
}