import { agent_loop_call_reserve, agent_loop_remaining_calls } from "std/agent/control"
import { __inject_feedback_with_tool_repair } from "std/agent/loop_tool_calls"
import { __agent_loop_max_consecutive_recoveries } from "std/agent/loop_turn_scope"
import { agent_emit_event } from "std/agent/state"
/**
* The three terminal grant allowances, and how many of each this run has left.
*
* A "grant" is permission to run one iteration PAST `current_max`. Nothing is
* withheld from working spend — the loop never shrinks its own budget — so read
* every number here as "extra iterations this run can still obtain", not as
* calls held in reserve. The three buckets:
*
* - `verification`: a failed terminal verify may re-open the loop, funded from
* `stall_config`'s `reserved_terminal_verify_iterations`.
* - `recovery`: bounded thrash rescues, funded from #7682's bound.
* - `terminal_report`: the one-shot terminal callback continue.
*
* ONE owner. Before this each bucket carried its own counter in a different
* module, and the projection kept a fourth number describing them from the
* outside, which is how the reported reserve drifted from what the loop would
* actually allow.
*
* `terminal_report` is 1 whenever a terminal callback is possible, NOT
* `final_wrapup ? 1 : 0`. The guard that governs it (`loop_terminal.harn`)
* tests only that no continue has been spent yet, so 1 is the effective
* allowance and the earlier `final_wrapup` condition mis-reported runs that had
* wrap-up disabled.
*
* @effects: []
* @errors: []
*/
pub fn __agent_loop_call_budget_new(opts: any, reserve_cfg: dict?) -> dict {
const verification = if (reserve_cfg?.reserved_terminal_verify ?? false)
|| (reserve_cfg?.zero_write_terminal_verify ?? false) {
to_int(reserve_cfg?.reserved_terminal_verify_iterations ?? 0) ?? 0
} else {
0
}
const allowance = agent_loop_call_reserve(
{
verification: verification,
// The recovery allowance has ONE owner (#7682). A literal here would be a
// second constant that silently drifts from the bound the recovery ladder
// actually enforces.
recovery: __agent_loop_max_consecutive_recoveries(),
terminal_report: 1,
},
)
return {
reserve: allowance,
remaining: {
verification: allowance.verification,
recovery: allowance.recovery,
terminal_report: allowance.terminal_report,
},
signal: opts?.call_budget_reserve_signal ?? false,
signalled: false,
iteration: 0,
max: nil,
}
}
/**
* How many grants this bucket has left.
*
* Unknown bucket names return 0 rather than nil: a caller that misspells a
* bucket gets "no grants available", which fails closed instead of handing out
* an unbounded one.
*
* @effects: []
* @errors: []
*/
pub fn __agent_loop_grants_remaining(state: dict?, bucket: string) -> int {
const remaining = state?.remaining
if bucket == "verification" {
return to_int(remaining?.verification ?? 0) ?? 0
}
if bucket == "recovery" {
return to_int(remaining?.recovery ?? 0) ?? 0
}
if bucket == "terminal_report" {
return to_int(remaining?.terminal_report ?? 0) ?? 0
}
return 0
}
/**
* May this bucket grant another iteration past the cap?
*
* @effects: []
* @errors: []
*/
pub fn __agent_loop_grant_allowed(state: dict?, bucket: string) -> bool {
return __agent_loop_grants_remaining(state, bucket) > 0
}
/**
* Record that a bucket spent one grant.
*
* Spending an exhausted bucket is a no-op rather than a negative count, so a
* caller that records without asking cannot manufacture budget.
*
* @effects: []
* @errors: []
*/
pub fn __agent_loop_record_grant(state: dict, bucket: string) -> dict {
if !__agent_loop_grant_allowed(state, bucket) {
return state
}
const remaining = state.remaining
if bucket == "verification" {
return state + {remaining: remaining + {verification: remaining.verification - 1}}
}
if bucket == "recovery" {
return state + {remaining: remaining + {recovery: remaining.recovery - 1}}
}
return state + {remaining: remaining + {terminal_report: remaining.terminal_report - 1}}
}
/**
* Project the remaining call budget BEFORE the turn spends anything, so a
* shortfall is visible while the agent can still act on it instead of being
* discovered in the terminal region.
*
* This measures and warns. The emitted `reserve` is the grant allowance this
* run started with, and a reader of
* `harn.agent_loop_call_budget_projection.v1` should treat `reserve` and
* `reserve_shortfall` as a forecast about whether the remaining budget can
* cover the terminal work.
*
* `state` carries the run's allowances and latch plus this turn's `iteration`
* and `max`, so the loop hands over one value instead of threading four.
*
* @effects: [agent]
* @errors: []
*/
pub fn __agent_loop_project_call_budget(harness: Harness, session: dict, state: dict) -> dict {
const reserve = state.reserve
const current_max = state.max
const remaining_calls = agent_loop_remaining_calls(current_max, state.iteration)
// Only a run that HAD headroom can lose it. When the whole cap is no larger
// than the reserve, the shortfall is true from turn zero and carries no
// information, so signalling it would spend the agent's context to tell it
// something it could not have acted on.
const reserve_shortfall = remaining_calls != nil
&& current_max > reserve.total
&& remaining_calls <= reserve.total
&& !state.signalled
agent_emit_event(
harness.agent,
session.session_id,
"typed_checkpoint",
{
schema: "harn.agent_loop_call_budget_projection.v1",
kind: "call_budget_projection",
receipt_kind: "call_budget_projection",
iteration: state.iteration,
remaining: remaining_calls,
// `measured` keeps an unavailable measurement distinguishable from a
// measured zero for every reader of this receipt.
measured: remaining_calls != nil,
max_iterations: current_max,
reserve: reserve,
grants_remaining: state.remaining,
reserve_shortfall: reserve_shortfall,
},
)
if reserve_shortfall && state.signal {
__inject_feedback_with_tool_repair(
harness.agent,
session.session_id,
"call_budget_reserve",
"Remaining model calls ("
+ to_string(remaining_calls)
+ ") no longer cover the "
+ to_string(reserve.total)
+ " calls reserved for verification, recovery, and terminal reporting. "
+ "Finish and report what you have now rather than starting new work.",
)
}
return state + {signalled: state.signalled || reserve_shortfall}
}