Skip to content

ADR-206: Closed-vocabulary annealing actions with tiered escalation and epistemic ledger#403

Merged
aaronsb merged 3 commits into
mainfrom
adr-206-annealing-vocabulary-and-escalation
May 23, 2026
Merged

ADR-206: Closed-vocabulary annealing actions with tiered escalation and epistemic ledger#403
aaronsb merged 3 commits into
mainfrom
adr-206-annealing-vocabulary-and-escalation

Conversation

@aaronsb
Copy link
Copy Markdown
Owner

@aaronsb aaronsb commented May 22, 2026

Summary

Draft ADR extending ADR-200 to address three layered defects discovered during testing:

  1. The current proposal_type ∈ {promotion, demotion} vocabulary is too narrow. Diagnostics on proposals 35/36/37 in kg_api.annealing_proposals show the LLM correctly identified a needed split-into-existing action ("carve a sub-cluster from atlassian-api-bitbucket-dc into existing atlassian-api-bitbucket-cloud"), but the schema had no slot for that intent — it fell back to "promote new ontology" with a colliding name and failed identically three times across three cycles.
  2. The LLM prompt doesn't see the existing ontology namespace, so name collisions are structurally inevitable.
  3. Only Sonnet-tier reasoning is available; there's no path to escalate genuinely ambiguous cases to a higher-capability model or to a human.

What this ADR proposes

Four phases — each independently shippable as a separate PR after this design is accepted:

Phase 1 — Closed action vocabulary. Replace binary promotion/demotion with a 7-action closed menu (SPLIT_NEW, SPLIT_INTO_EXISTING, MERGE, DECOMPOSE_TO_PRIMORDIAL, RENAME, NO_ACTION, ESCALATE). Each action is structurally self-contained. LLM prompt grows to include the full ontology inventory. Pins the primordial pool as a permanent, undeletable reservoir — dissolution never destroys concepts.

Phase 2 — Tiered escalation cascade. Sonnet classifies. Opus arbitrates as "evaluate the evaluator" (Sonnet's prompt and response are quoted as evidence in XML tags, not merged into Opus's task). Human is the final tier, with multi-turn dialogue support. The cascade is configured per platform via escalation_chain — same scope as model provider / API key. Proposal reasoning lives in a new annealing_proposal_messages table. GC becomes a load-bearing invariant: every proposal MUST reach a terminal state.

Phase 3 — Control surface and self-regulation. Tuning knobs (min_activity_for_cycle, min_ontology_age_epochs, golden_path_confidence, failure_cooldown_epochs, etc.) exposed via CLI/API/web. Opus can tune operational knobs via an ADJUST_CONTROL action; safety knobs (automation_level, escalation_chain, opus_confidence, phone_a_friend_cost_budget) remain admin-only rails.

Phase 4 — Epistemic ledger. The proposal queue + reasoning chains become a permanent, mineable decision log. Terminal proposals are forever (no purging). Schema adds signal_embedding, signal_payload, signal_kind, outcome_quality (async-set), superseded_by, graph_delta_summary. Opus's arbitration prompt becomes a RAG agent that injects retrieved similar past decisions as context. Calibration (confidence-vs-outcome correlation, human-vs-Opus agreement) becomes a closed loop feeding back into auto-tuning, not just observability.

Open design questions — please weigh in

Three were surfaced during drafting and remain unresolved:

  1. Outcome-quality scoring function. Phase 4 defines outcome_quality as numeric, async-set, derived from post-execution graph metrics — but the metric basket (coherence drift, mass drift, cross-edge ratio change, …) is unspecified. This is load-bearing for Phase 4's calibration loop; needs calibration against real ledger data before pinning.
  2. phone_a_friend_cost_budget as safety rail. Listed admin-only in the control table; the draft explicitly added it to the list of knobs Opus cannot self-tune. Confirm this is the intended behavior — a runaway Opus that can widen its own cost budget defeats the budget.
  3. Mid-cycle control-change UX. The snapshot-at-cycle-start mechanism resolves consistency, but not how an operator learns "your threshold change is queued for cycle N+1." Open on the operator-facing affordance.

Scope of this PR

This is a Draft PR for design review, not for merge. The ADR is Status: Draft; once the open questions are resolved and reviewers concur on direction, status moves to Accepted and Phase 1-4 implementation work branches off.

Related

Test plan

This is a documentation-only PR; no code changes.

  • docs/scripts/adr lint is clean (verified locally)
  • Reviewers read the Context section and confirm the failure framing matches their understanding
  • Reviewers respond to the three open design questions
  • Mermaid diagrams render correctly in GitHub's preview (cascade, state machine, action-menu tree)

aaronsb added 3 commits May 21, 2026 06:52
Two coupled fixes for Step 6 init failure ("Anthropic requires an
embedding provider") on first-run setup.

1. Reorder guided-init: configure the local embedding profile right
   after admin user creation (new Step 4), before AI provider selection.
   The wizard already collects the GPU/CPU choice at the top; map that
   to a PyTorch device string (mac→mps, nvidia/amd/amd-host→cuda,
   cpu→cpu) and pass it to `configure.py embedding --device`. The API
   container picks up the active profile + device at startup. Renumber
   subsequent steps (AI provider → 5, validate key → 6, model select →
   7); Garage and start-app stay at 8 and 9.

2. SDK-direct catalog refresh. `models refresh` previously went through
   get_provider(), which instantiates AnthropicProvider — whose __init__
   eagerly constructs an OpenAIProvider as the embedding delegate. The
   operator container has no loaded EmbeddingModelManager (only the API
   container initializes one at startup), so get_embedding_provider()
   returns None and the eager fallback fails for lack of an OpenAI key.
   New _fetch_catalog_via_sdk() bypasses __init__ via __new__, sets
   only the SDK client (or api_key for OpenRouter), and reuses the
   existing fetch_model_catalog method. Mirrors the SDK-direct pattern
   already used by _validate_provider_key.

Adds a --device flag to `configure.py embedding` so the wizard can
write the chosen device onto the activated profile in one call.
set_model_default fetched the provider/category for the target row and
then unpacked the result with `provider, category = row`. When the
caller's connection is configured with RealDictCursor (as the operator
container's configure.py is — see operator/configure.py line 39), the
row is a dict subclass and tuple unpacking silently yields the column
*names* — "provider" and "category" — rather than the values. The
clear-existing-default UPDATE then matched zero rows, and the
set-new-default UPDATE collided with the still-set old default,
violating idx_catalog_default on (provider, category).

The API container's path didn't hit this because AGEClient.pool doesn't
set a cursor_factory; only this operator-driven path tripped on it.

Replace the SELECT + tuple-unpack with a subquery so the function is
cursor-factory-agnostic. As a bonus the path is now idempotent: setting
a model that's already the default no longer races with itself.

Manifests as Step 7 of guided init: "Models command failed: duplicate
key value violates unique constraint 'idx_catalog_default'".
ADR-200's binary promotion/demotion vocabulary cannot represent the
merge-into-existing intent that diagnostics on proposals 35/36/37
showed the annealing agent actually needs — the LLM's own reasoning
recognized the existing target ontology and recommended merging into
it, but had no schema slot to express that, so the proposal failed
identically across three cycles. This ADR redesigns the action menu
as a closed 7-option set, introduces a tiered Sonnet → Opus → Human
escalation cascade, makes the proposal queue a permanent epistemic
ledger that Opus can mine when arbitrating, and exposes tuning
controls so operators (and Opus, within safety rails) can
self-regulate the cycle.

Status: Draft — open for review on three unresolved design questions
(outcome-quality metric basket, cost-budget safety rail, mid-cycle
control-change UX).

Related: ADR-200, #402
@aaronsb aaronsb marked this pull request as ready for review May 23, 2026 03:49
@aaronsb aaronsb merged commit 0dc9096 into main May 23, 2026
3 checks passed
@aaronsb aaronsb deleted the adr-206-annealing-vocabulary-and-escalation branch May 23, 2026 03:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant