ADR-0028: Two-tier LLM for Digital Worker — local triage as an unbilled pre-gate filter

Status

Accepted (pending Phase 1 implementation)

Tags

ai, llm, digital-worker, ollama, triage, access-gate, cost, chat

Decision

Digital Worker proactive task extraction runs in two stages:
  1. Local triage (pre-gate filter, unbilled). A small local model (Ollama, already in the AI provider factory) runs over a rolling window of recent messages and classifies whether the window contains a real commitment / request / blocker / handoff / due-date. It deliberately does not pass through AccessGate — it is not billed, consumes no org caps, and its decision + score is logged on dw_chat_events.
  2. Remote extraction (gated, billed). Only on a positive triage hit does the frontier model run through AccessGate (digital_worker:chat_task_extract) to draft the task. One call per coalesced commitment.
Mention-response skips triage entirely — it is a solicited request and goes straight to the gated remote model (digital_worker:mention_response). If the local triage model is unreachable, the proactive path fails closed (skip + log); it never escalates to the remote model.

Why

Taken literally, the original design ran an LLM call on every chat message. That burns AccessGate caps on chatter (“ok”, ”👍”) and fragments commitments that span several messages. This is the same problem Fieldforce solved with a deterministic shortlist feeding a single gated LLM call (ADR-0022/0023) — here the shortlist step is a learned local classifier instead of keyword heuristics, which classifies far better while staying cheap. The part worth recording is the deliberate gate bypass. The codebase invariant (CONTEXT.md, Access Gate) is “every AI call passes through the gate.” A future reader seeing an ungated Ollama inference will read it as a bug or a hole. It is neither: local triage is gating logic — the worker’s equivalent of a deterministic shortlist — not billable product output. Routing it through the gate (ADR option B during design) would let a free local call be “denied” by org caps, which is nonsensical. So the billable boundary is drawn at the remote call only. Rejected alternatives:
  • Per-message remote extraction. Cost scales with chatter; fragments multi-message commitments. Rejected.
  • Windowed/batched remote with a lease, no local triage. Works, but still pays for noise-only windows. Rejected in favor of the cheaper learned pre-filter.
  • Keyword/heuristic pre-filter. Cheaper than a local model but materially worse at distinguishing “I’ll handle it” from “I’ll grab lunch.” Rejected.
  • Local triage routed through AccessGate (own feature key, zero cost). Honors the invariant strictly but allows a free local call to be cap-denied. Rejected as incoherent.

Consequences

  • Introduces a local-inference dependency (Ollama) on the proactive path. Mention-response is unaffected.
  • Triage recall bounds extraction recall — a commitment the local model misses never reaches the remote model. Acceptable; the human is still in the channel.
  • Fail-closed means a local-model outage silently pauses proactive drafting (logged on dw_chat_events), protecting remote spend rather than escalating it.
  • Cap/credit/provider outages on the remote call interact with the extraction watermark — see ADR-0030.

Rules for agents

  • The local triage call MUST NOT be routed through AccessGate and MUST NOT be billed; log its decision on dw_chat_events.
  • The remote extraction and mention-response calls MUST pass through AccessGate with their feature keys.
  • A local-triage outage MUST fail closed (skip proactive extraction), never escalate to the remote model.