Repository navigation
feat(agent-seam): one typed-decision seam for code-consumed verdicts, local-backend-first #17308
Description
Activity
- addedenhancementNew feature or requestNew feature or requestarea: agent-seamWave 3 · cluster H — Agent seam & tool governanceWave 3 · cluster H — Agent seam & tool governance
on Sep 23, 2026 - added 18 commits that reference this issue
on Sep 23, 2026 Closed by #17327, merged as
2b46cea74f. Evidence per criterion, verified withgit 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 carriesCalibration(:91) and the local backend reportsSELF_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 toMEASUREDis the benchmark below and nothing else.AC2 — pluggable backend, local by default, no new outbound dependency.
LocalModelBackend(:298) routes throughservices.llm_servicewithLLMType.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:286on aChoiceQuestion;_parse_agreementreturns zero matches on merged main.judges/__init__.py:200goes through the same validated loop withJUDGMENT_SCHEMArather than the three primitives, because its payload carries per-dimensioncriterion_scoresandimprovement_suggestionsthat choice/score/boolean cannot express and that_extract_safety_scorereads — 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.pywith 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.pyasserts 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.:107test_the_detector_finds_a_seam_import_where_one_existsis the positive control, verified by mutation — with_importsreturning 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.
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)autobot-backend/services/claim_verifier.py:287(fan-out at:317)autobot-backend/judges/__init__.py:157autobot-backend/orchestrator.py:840_score_plan→:862_select_best_planautobot-backend/services/knowledge/contradiction_detector.py:214autobot-backend/services/autoresearch/scorers.py:206:240autobot-backend/agents/agent_orchestration/routing.py:340:315regex /:321learned /:326RL run first)autobot-backend/rlm/evaluator.py:64autobot-backend/chat_workflow/graph.py:855inline response judgeAUTOBOT_CHAT_INLINE_JUDGE,:833)Where this explicitly does NOT apply
Audited and rejected — recording so a later reader does not re-propose them:
autobot-backend/llm_shared/tiered_routing/complexity_router.py:42pluscomplexity_scorer.py:24(six weighted factors at:40-45) decide the tier with no model call; provider choice is a fixed cascade atllm_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.autobot-backend/intent_classifier.py:51,workflow_classifier.py:205,services/knowledge/intent_detector.py:120,agent_tier_classifier.py:119.autobot-backend/advanced_rag_optimizer.py:691-704.autobot-backend/security/prompt_injection_detector.py:361. Putting a steerable model on that decision would be the wrong direction.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:233runs 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 probabilitiesseam besidellm_shared/structured_ops.py:16extract():Acceptance criteria
services/claim_verifier.py:287,judges/__init__.py:157) are migrated, with their bespoke parsers deletedguard_egresspassed explicitly (autobot_shared/http_client_manager.py:215defaults it toNone= unguarded) and throughautobot_shared/security/redaction.py; its key is registered viaservices/provider_key_vault.py:73and SSOT, never a literalNotes
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