Skip to content

feat(agent-seam): one typed-decision seam for code-consumed verdicts, local-backend-first #17308

Description

@mrveiss

Problem

AutoBot makes ~8 kinds of small, frequent, code-consumed judgements, each through a full free-text LLM call with a bespoke parser. There is no shared seam for "decide this, typed, with a probability" — so every site reinvents the prompt, the output format, the parser and the threshold.

An external vendor category has appeared for exactly this shape: decision-only models that take a state plus typed questions (choice / score / boolean) and return typed values with calibrated probabilities instead of text, at roughly two orders of magnitude less cost and latency than a frontier LLM call. The performance claims are self-reported and unverified, the models are closed-weight, and at least one is documented by its own vendor as steerable by adversarial content in the state — so this issue is about owning the interface, not about adopting a vendor.

The call sites (audited on main)

Site Decision Shape
autobot-backend/services/claim_verifier.py:287 (fan-out at :317) AGREE / CONTRADICT / UNRELATED per (claim, source) choice, N calls per claim — highest volume
autobot-backend/judges/__init__.py:157 score + APPROVE/REJECT/CONDITIONAL/REVISE + confidence + per-dimension scores choice + score + boolean in one round trip
autobot-backend/orchestrator.py:840 _score_plan → :862 _select_best_plan plan quality 0–1 score, × N candidate plans
autobot-backend/services/knowledge/contradiction_detector.py:214 conflict pairs per chunk group batch-shaped
autobot-backend/services/autoresearch/scorers.py:206 rating 0–10, normalized at :240 score
autobot-backend/agents/agent_orchestration/routing.py:340 which agent handles a request (last-resort tier; :315 regex / :321 learned / :326 RL run first) choice, low volume by construction
autobot-backend/rlm/evaluator.py:64 ACCEPT / REFINE partial fit — the CRITIQUE/HINT text feeds refinement, so a decision-only model cannot replace this whole call
autobot-backend/chat_workflow/graph.py:855 inline response judge reply quality flag-off today (AUTOBOT_CHAT_INLINE_JUDGE, :833)

Where this explicitly does NOT apply

Audited and rejected — recording so a later reader does not re-propose them:

  • Model/provider routing is already deterministic and local. autobot-backend/llm_shared/tiered_routing/complexity_router.py:42 plus complexity_scorer.py:24 (six weighted factors at :40-45) decide the tier with no model call; provider choice is a fixed cascade at llm_shared/provider_registry.py:414. There is no LLM cost to displace, and a network call would add latency and a dependency to something that currently costs microseconds.
  • Fast-path classifiers are already sub-10ms and local: autobot-backend/intent_classifier.py:51, workflow_classifier.py:205, services/knowledge/intent_detector.py:120, agent_tier_classifier.py:119.
  • Reranking is a local cross-encoder, not an LLM — autobot-backend/advanced_rag_optimizer.py:691-704.
  • Prompt-injection detection is deterministic regex — autobot-backend/security/prompt_injection_detector.py:361. Putting a steerable model on that decision would be the wrong direction.
  • Security gates. The pre-action verifier (autobot_shared/pre_action_verifier_guard.py:298) is the best-fitting shape in the codebase — its entire output is one float — and is precisely where a cheaper, injection-steerable classifier must not go. It also deliberately picks a provider different from the actor (:273) to avoid shared-context steering.

What AutoBot already has

autobot-backend/agents/gemma_classification_agent.py:233 runs a small model through Ollama (:296) with a keyword fallback (:418) — zero per-call cost, zero egress, zero vendors. That is the existing in-house answer to this problem, and it should be the default backend behind the seam.

Proposal

Add a decide(state, questions) -> typed answers with probabilities seam beside llm_shared/structured_ops.py:16 extract():

  • question primitives: choice (probability per option), score (ordered levels), boolean (probability)
  • multiple questions evaluated against one shared state in a single round trip
  • pluggable backend: the local small-model path by default; a hosted decision model only as an optional backend, behind a benchmark, never on a security gate
  • call sites above migrate to it, replacing their bespoke prompt + parser + threshold

Acceptance criteria

  • A single typed-decision interface exists with the three question primitives and returns calibrated probabilities, not just labels
  • Backend is pluggable and defaults to the local small-model path — no new outbound dependency is required to use the seam
  • At least the two highest-volume sites (services/claim_verifier.py:287, judges/__init__.py:157) are migrated, with their bespoke parsers deleted
  • If a hosted backend is enabled it goes through the guarded fetch with guard_egress passed explicitly (autobot_shared/http_client_manager.py:215 defaults it to None = unguarded) and through autobot_shared/security/redaction.py; its key is registered via services/provider_key_vault.py:73 and SSOT, never a literal
  • A benchmark records accuracy and latency of each backend against a labelled sample before any hosted backend becomes a default — a calibration claim without our own measurement is not evidence
  • Documented exclusion list: the seam is not used for provider/tier routing, injection detection, or the pre-action verifier gate

Notes

The endpoint shape of the hosted models in this category is not OpenAI chat-completions compatible (no messages array, no temperature — a state-plus-questions request shape), so such a backend cannot register as a normal provider next to autobot-backend/llm_shared/providers/openai_compatible.py. It would sit outside the fallback chain (provider_registry.py:414), the degradation skip (:464-476) and the cost tracking landed in #16230 — those gaps are part of the cost of enabling one.

Implementation Order

Wave: 3
Depends on: the provider structured-output and parse-hardening children of this umbrella

Activity

  1. mrveiss commented on Sep 24, 2026

    @mrveiss
    OwnerAuthor

    Closed by #17327, merged as 2b46cea74f. Evidence per criterion, verified with git show origin/main:<path>. Two criteria are deliberately left unticked with their reasons — an unticked criterion with a reason is a finding; a ticked one with a reason is an argument nobody re-reads.

    AC1 — one typed interface with the three primitives, returning probabilities. llm_shared/decisions.py: ChoiceQuestion (:109), ScoreQuestion (:147), BooleanQuestion (:184), decide (:351) answering all of them against one shared state in a single round trip, validated against a generated JSON Schema.

    ⚠️ The word "calibrated" is NOT claimed. Every answer carries Calibration (:91) and the local backend reports SELF_REPORTED: the number is the model's stated confidence, not a measured frequency. Nobody here has measured calibration, so asserting it would be exactly the evidence-shaped guess this issue's own last criterion warns about. The path to MEASURED is the benchmark below and nothing else.

    AC2 — pluggable backend, local by default, no new outbound dependency. LocalModelBackend (:298) routes through services.llm_service with LLMType.CLASSIFICATION, the path AutoBot already sends to a local model; register_backend (:325) exists so an alternative can be measured against it. No hosted backend was added, so the seam costs no egress.

    AC3 — the two highest-volume sites migrated, bespoke parsers deleted. claim_verifier.py:286 on a ChoiceQuestion; _parse_agreement returns zero matches on merged main. judges/__init__.py:200 goes through the same validated loop with JUDGMENT_SCHEMA rather than the three primitives, because its payload carries per-dimension criterion_scores and improvement_suggestions that choice/score/boolean cannot express and that _extract_safety_score reads — migrating it to the primitives would have silently dropped them. The hand-indexing parser is deleted either way, and both paths share one loop underneath. That reading was flagged in the PR body before review rather than after.

    AC4 — hosted-backend egress, redaction and key handling: NOT TICKED, not applicable. No hosted backend exists, so the criterion has no subject. The conditions for adding one are written into the seam's module docstring and into #17326's notes rather than ticked away.

    AC5 — a benchmark before any hosted backend becomes a default. scripts/benchmark_decision_backends.py with a checked-in labelled sample (8 cases across all three primitives, both outcomes represented). It reports accuracy over answered cases with refusals counted separately, p50/p95 latency measured here rather than quoted, and the separation between mean probability on right and wrong answers — a backend whose two means are equal is reporting a number with no information in it, whatever it calls it. Fifteen tests cover the harness's own arithmetic against hand-built results, because an instrument never read against a known input is not evidence.

    AC6 — documented exclusion list, as a test rather than a paragraph. decisions_exclusions_test.py asserts that tier routing, the four fast-path classifiers, reranking, injection detection and the pre-action verifier gate do not import the seam, and fails if one of those files is renamed out from under the check.

    That guard needed a fix of its own before it meant anything, found by asking whether it would still pass if the thing under test were deleted: every case asserted an absence, so a blinded _imports() left the whole file green. :107 test_the_detector_finds_a_seam_import_where_one_exists is the positive control, verified by mutation — with _imports returning an empty set the suite goes 1 failed / 11 passed, where before it was 11 passed / 0 failed.

    Follow-up filed: #17326 for the four audited sites not migrated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions