import {
agent_tool_lifecycle_error_category,
agent_tool_result_is_ok,
agent_tool_result_outcome,
} from "std/agent/tool_lifecycle"
/**
* Host dispatches are canonically a result list; a turn without tools has no
* dispatch. Normalize the absent case here so downstream policy stays shape
* agnostic.
*/
pub type AgentToolDispatch = list?
pub fn __dispatch_results_list(dispatch: AgentToolDispatch) -> list {
return dispatch ?? []
}
fn __error_category_is_dispatch_rejection(category: any) -> bool {
return category == "schema_validation" || category == "permission_denied"
}
/**
* Dispatch rejection from STRUCTURED fields only: the outer envelope's typed
* error category (via `agent_tool_result_outcome`), or the nested host body
* `result.error` / `result.error_category` that denial producers already stamp.
* Never substring-matches rendered prose.
*
* @effects: []
* @errors: []
*/
pub fn __tool_result_dispatch_rejection(result: dict) -> bool {
if type_of(result) != "dict" {
return false
}
const outcome = agent_tool_result_outcome(result)
if __error_category_is_dispatch_rejection(outcome.error_category) {
return true
}
const nested = result?.result
if type_of(nested) == "dict" {
const nested_cat = agent_tool_lifecycle_error_category(nested?.error_category ?? nested?.error)
if __error_category_is_dispatch_rejection(nested_cat) {
return true
}
}
return false
}
pub fn __tool_result_product_error(result: dict) -> bool {
if type_of(result) != "dict" {
return false
}
const outcome = agent_tool_result_outcome(result)
if outcome.product_error != nil {
return outcome.product_error ? true : false
}
return __tool_result_dispatch_rejection(result)
}
pub fn __tool_result_ok(result: dict) -> bool {
const outcome = agent_tool_result_outcome(result)
const structured = agent_tool_result_is_ok(outcome)
if structured == false {
return false
}
if __tool_result_product_error(result) {
return false
}
if structured == true {
return true
}
if result?.ok != nil {
return result.ok ? true : false
}
if result?.success != nil {
return result.success ? true : false
}
const status = result?.status ?? ""
return status == "ok" || status == "success"
}
/**
* The outcomes a tool call can end in that the AGENT did not choose.
*
* A call the user cancelled, or one the loop abandoned while exiting, is not
* evidence about whether the agent gave up: someone else ended it. Counting
* either as a failed attempt would attribute a user's cancel to the model, and
* a run whose only tool call the user cancelled would be reported as abandoned
* by the agent.
*/
const __AGENT_TOOL_NOT_THE_AGENTS_OUTCOME = ["cancelled", "abandoned_at_loop_exit"]
/**
* The typed error category of a failing tool result, from STRUCTURED fields
* only: the outer envelope's category, else the nested host body's. Never a
* substring match on rendered prose.
*
* Returns `unclassified` for a result that failed carrying no typed category,
* so a reader can tell "failed with no category" from "did not fail" instead
* of both arriving as nothing.
*
* @effects: []
* @errors: []
*/
pub fn __tool_result_failure_category(result: dict) -> string? {
if type_of(result) != "dict" || __tool_result_ok(result) {
return nil
}
const nested = result?.result
const nested_cat = if type_of(nested) == "dict" {
agent_tool_lifecycle_error_category(nested?.error_category ?? nested?.error)
} else {
nil
}
const resolved = agent_tool_result_outcome(result).error_category ?? nested_cat
if resolved == nil {
return "unclassified"
}
return to_string(resolved)
}
/**
* How many tool calls a dispatch made and how many of them worked.
*
* Deliberately NOT derivable from the session's `successful_tools_seen` and
* `rejected_tools_seen`. Those lists deduplicate by tool NAME, so their lengths
* answer "which tools worked", not "how many calls were made". A terminal that
* printed one while calling it the other would be a mislabeled instrument, and
* telling two abandoned runs apart is the whole point of this evidence.
*
* Calls that ended in an outcome the agent did not choose are counted in
* neither total, so they cannot convict and cannot acquit.
*
* @effects: []
* @errors: []
*/
pub fn __dispatch_call_tally(dispatch: AgentToolDispatch) -> dict {
let attempted = 0
let succeeded = 0
for result in __dispatch_results_list(dispatch) {
if type_of(result) != "dict" {
continue
}
const category = __tool_result_failure_category(result)
if category != nil && contains(__AGENT_TOOL_NOT_THE_AGENTS_OUTCOME, category) {
continue
}
attempted = attempted + 1
if __tool_result_ok(result) {
succeeded = succeeded + 1
}
}
return {attempted: attempted, succeeded: succeeded}
}
/**
* The typed category of the last counted failure in a dispatch.
*
* Skips the same outcomes the tally skips, so the reported category always
* names something the counts actually reflect.
*
* @effects: []
* @errors: []
*/
pub fn __dispatch_last_rejection_category(dispatch: AgentToolDispatch) -> string? {
let category = nil
for result in __dispatch_results_list(dispatch) {
const resolved = __tool_result_failure_category(result)
if resolved != nil && !contains(__AGENT_TOOL_NOT_THE_AGENTS_OUTCOME, resolved) {
category = resolved
}
}
return category
}
pub fn __tool_result_name(result: dict) {
return result?.tool_name ?? result?.name ?? ""
}
pub fn __tool_names_by_status(dispatch: AgentToolDispatch, want_ok: bool) -> list<string> {
const results = __dispatch_results_list(dispatch)
let names = []
for result in results {
const name = __tool_result_name(result)
if name != "" && __tool_result_ok(result) == want_ok {
names = names.appending(name)
}
}
return names
}