Skip to content

Tool-failure classification: approval denial → 'try again', TTL expiry → auto-retry Timeout, failure missing from snapshot #4459

Description

@senamakel

Part of the v0.58.7 → HEAD TinyAgents-migration audit (see parent issue). Repairs tool-call failure classification (#4413 rework, commit b5837ed).

A. User approval denial classified as recoverable "try again"

The classifier short-circuits only POLICY_BLOCKED_MARKER ("[policy-blocked]") (src/openhuman/tinyagents/middleware.rs:1411-1424). The entire approval-gate deny family emits POLICY_DENIED_MARKER ("[policy-denied]", src/openhuman/security/policy/types.rs:26): user deny, dropped channel, subconscious-tainted origin, install_tool.rs:129/136, composio/tools.rs:802. The heuristic's intended needle "policy denied" (with a space, src/openhuman/tool_status/ops.rs:55) matches no producer — dead code.
Scenario: user clicks Deny on ApprovalRequestCard → classified UnknownFailureCategory::Recoverable → UI: "Something went wrong with this action. Try again…" for an action the user just refused; Unknown is documented as eligible for one bounded auto-retry, i.e. auto-re-attempting a user-refused external effect.

B. Approval TTL expiry classified as execution Timeout promising auto-retry

Deny reason "[policy-denied] Approval for '{tool}' timed out after {N}s…" (src/openhuman/approval/gate.rs:703) hits the classifier's first check ("timed out", tool_status/ops.rs:34) → ToolFailureClass::Timeout → copy: "The action took too long and was stopped. OpenHuman will try again…". Nobody approved the action.

C. Backend never persists failure in the turn-state snapshot

ToolTimelineEntry has no failure field (src/openhuman/threads/turn_state/types.rs:58-73); turn_state/mirror.rs:138-153 destructures and drops it. The merged frontend expects it (app/src/types/turnState.ts:121-145, app/src/store/chatRuntimeSlice.ts:732-734): a failed tool's explanation survives live but disappears on thread switch/restart.

D. Smaller gaps

  • Subagent failures are classified then discarded — SubagentToolCallCompleted has no failure field (src/openhuman/tinyagents/observability.rs:518-545, src/openhuman/agent/progress.rs:179-193).
  • Marker/timeout sniffs read result.content while classify-text prefers result.error (middleware.rs:1413-1423) — latent asymmetry; and success == error.is_none() makes the unwrap_or(content) fallback dead.

Fix plan

  1. Add a POLICY_DENIED_MARKER short-circuit before the heuristics → new class (e.g. ToolFailureClass::Denied) → FailureCategory::UserDeclined (non-retryable) with honest copy ("You declined this action" / "Approval expired before anyone responded").
  2. Check markers before the "timed out" sniff so TTL expiry classifies as Denied/ApprovalExpired, never Timeout.
  3. Add failure: Option<PersistedToolFailure> to ToolTimelineEntry + thread through mirror.rs, matching the shape in app/src/types/turnState.ts.
  4. Add failure to SubagentToolCallCompleted and populate it from the already-computed classification.
  5. Normalize the classifier to sniff both error and content consistently; delete the dead "policy denied" needle.

Acceptance criteria

  • Deny → UI shows a "declined" explanation, no retry offer, no auto-retry eligibility. TTL expiry → "approval expired", no auto-retry promise.
  • Failed-tool explanations survive thread switch/app restart (snapshot round-trip test against the frontend contract).
  • Subagent failed rows carry an explanation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    agentBuilt-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/.bugpriority: highBreaks a specific featuresubtaskSubtask of a larger tracked effort.

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    Status
    Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions