# Tool dispatch invariants

- Provider schemas and executor validation must describe the same action
  contract.
- Until dispatcher schemas are emitted per variant, pre-flight validation may
  remove a field only when it is declared by another variant, forbidden by the
  selected variant, and revalidation succeeds.
- Every recovery is loud (`VARIANT_RECOVERED`, `COERCED`, or `INFERRED`) and is
  written to both trace sinks.
- Unknown fields, typos, and semantically ambiguous calls remain errors.
- Tool-catalog discovery filters are typed matrx-orm predicates. Prefix lookup
  uses a bound case-insensitive `Like` expression; no SQL fragment is accepted
  from tool arguments.
- `timeout_seconds` is a hard cancellation boundary only for ordinary tools.
  A tool marked `must_complete` treats that value as a loud soft deadline:
  execution continues to its real terminal result, retries remain forbidden,
  and request cancellation is propagated only after the result is persisted.
  Paid child-agent tools always opt into this policy.
- Tool helpers that need an agent **declare semantic variables and run a DB-backed
  `NamedAgent` through `matrx_ai.mandates.run_mandated`**. System prompts never live
  in tool source. `summarize_content` declares `instructions` and `content`; its
  DB user template preserves the former prose composition exactly, while the
  caller retains the 100,000-character content cap and per-run model override.
- A tool that produces image bytes funnels through
  `image_outputs.normalize_tool_output_async` **before** anything else touches
  the output, never through `media.persist_media_blobs_async` (non-image blobs
  only). The order is load-bearing: the legacy persister deletes the `*_base64`
  key the funnel detects on, so running it first silently skips the funnel —
  leaving an expiring signed URL with no `file_id` in permanent chat history and
  no `ImageContent` block, which makes the model blind to the image it just
  produced. Guard: `tests/test_media_identity_consumers.py`.
- The administrative `sql` tool is a historical name, not an escape hatch.
  Its query action accepts only a registered table, exact-match predicates,
  explicit fields/order, and pagination. Non-super-admin reads cross the host
  seam as structured values and execute inside the caller's RLS identity.
- Tool-call completion and message-link updates queued on a captured request
  coordinator use the exact `chat.tool_call` registry identity. This remains
  true in detached completion tasks, where the ambient coordinator context is
  intentionally unavailable and the captured coordinator is passed directly.
  A bare target is a queue-time drop and therefore a fatal commit-barrier error.
- An `AgentToolSpec` may carry a per-projection `max_recursion_depth`; `None`
  preserves the platform default. A recursion-depth refusal of an agent tool
  emits the additive `member_depth_exhausted` warning before the normal error
  result is persisted, so truncated delegation is visible to the human without
  changing executor control flow.
- `drain_pending` owns the single safe pre-provider-call reconciliation point:
  after in-memory mutations and DB injections, it invokes the optional
  host-provided iteration tool refresh. The hook is absent for standalone
  matrx-ai, may return a rebound context, and logs loudly while retaining the
  prior toolset if the host refresh fails.
