feat(intake): Phase 8.0 IntakeIntentRouter — Haiku-backed dispatcher with explicit-hint short-circuit#6
Merged
Conversation
…with explicit-hint short-circuit
First implementation PR of the Phase 8 feeding restructure. Ships the
harness primitive that 8.1 (food extractor), 8.3 (hazard gate), and
8.4 (capture-flow branching) all plug into. Pure dispatcher; no UI;
no WikiRepo changes; no agent_loop wiring.
## What landed
- `lib/harness/intake/intent_router.dart` — new file. Defines:
- `IntakeIntent` flat enum (`logMealAfter`, `checkMealBefore`,
`generalMemory`) with `fromIdOrFallback` drift-tolerant factory,
mirroring the `PhotoSetting` / `PhotoActivity` precedent at
photo_extractor.dart:159–190.
- `IntakeIntentRouter` class. Constructor takes `LlmClient`,
`VisionGate`, optional 8s timeout. `resolve(...)` signature:
`imageBytes` + optional `userCaption` + optional `explicitHint`
+ `mediaType`. Returns non-nullable `Future<IntakeIntent>`.
Locked system prompt + user-instruction const.
- `lib/app/providers.dart` — `intakeIntentRouterProvider` wired
against `haikuLlmClientProvider` (not `llmClientProvider`) per
DECISIONS row 41 (f) Sonnet/Haiku split precedent. Imports
threaded.
- `test/harness/intake/intent_router_test.dart` — 20 unit tests
using the inline `_MockLlm` + `_BlockingGate` pattern from
photo_extractor_test.dart. Covers: explicit-hint short-circuit
(parameterized across all enum values), each soft-case →
intent mapping, prompt + ImageBlock anchors, caption threading
(present + whitespace-only), code-fence stripping, every
always-safe fallback path (malformed JSON, missing field, drift
string, non-object, transport throw, timeout, gate block), and
drift tolerance of `fromIdOrFallback` directly.
- `ROADMAP.md` — 8.0 `[ ]` → `[x]`.
- `DECISIONS.md` — row 104 appended.
## Hybrid resolution contract (row 98 + 104)
- `explicitHint != null` → returned immediately; no LLM call,
no gate check.
- `explicitHint == null` → gate check → Haiku classification on
the photo (+ optional caption) → JSON parse → enum value or
`generalMemory` fallback.
The router is the always-safe dispatcher. **Every failure mode
degrades to `IntakeIntent.generalMemory`**: gate block, LLM throw,
timeout, malformed JSON, missing field, JSON decoded to non-object,
unknown drift value. The capture flow never has to handle a null,
never has to surface an error toast. `generalMemory` preserves the
user's ability to log a memory regardless of classifier health.
## In-flight design corrections
The approved plan claimed "Haiku as classifier was considered +
deferred — would require transport per-call model override, which
the codebase doesn't have today." That claim was wrong. Reading
`lib/app/providers.dart` during implementation surfaced
`haikuLlmClientProvider` at line 279 (Phase 6 task 6.8, DECISIONS
row 41 (f) — "Sonnet for the extractor, Haiku for lightweight
calls"). The router is exactly the lightweight-call use case the
precedent locked. Implementation uses Haiku (matching the
`affectiveObserverProvider` wiring at line 287); DECISIONS row 104
captures the corrected rationale. Same router code; different
DI choice.
Plan also speculated a separate `intent_router_providers.dart`
file; reading the codebase showed all providers cluster in
`lib/app/providers.dart` (single hub pattern). Provider lives
there now, sibling to `photoExtractorProvider`.
## Locked design (DECISIONS row 104)
- Flat `IntakeIntent` enum + `fromIdOrFallback` factory.
- `IntakeIntent? explicitHint` parameter type (symmetry between
input and output; lens-extensible without parameter growth).
- Non-null `Future<IntakeIntent>` return + `generalMemory`
fallback on every failure path.
- 8s timeout (half of PhotoExtractor's 15s; runs upstream of
the extractor in the same form-preview window).
- Haiku transport via `haikuLlmClientProvider` (row 41 (f)).
- `VisionGate` routing for entitlement-path uniformity even
though intake is FREE per row 102.
- System-prompt bias rules ("when unsure prefer logMealAfter";
"when unsure prefer generalMemory") — deliberate "soft cases
get softer" amortization. Hazard gate (8.3) still fires on
either meal intent.
## Cost trade-off (deliberate accept)
Non-hinted food photos fire 2 vision calls: Haiku classification
+ Sonnet extraction. Intake is free per row 102; the 8.4 form UI
defaults to surfacing the before/after toggle, so the expected
real-user path ships an explicit hint and short-circuits the
classifier entirely. Worst case is one Haiku + one Sonnet; expected
case is one Sonnet.
## Verification
- `flutter analyze --fatal-infos; echo "exit=$?"` → `exit=0`,
"No issues found! (ran in 3.3s)".
- `flutter test --reporter expanded; echo "exit=$?"` → `exit=0`,
**1776 tests pass** (baseline 1756 + 20 new router tests; matches
plan's predicted ~1769 within noise).
- Targeted `flutter test test/harness/intake/intent_router_test.dart`
→ 20/20 pass in <1s.
- On-device: N/A — pure harness primitive, no UI. The router is
exercised end-to-end on a device in Phase 8.4's on-device gate.
## Out of scope (subsequent Phase 8 PRs)
- 8.1 food-mode extractor (separate Sonnet vision call branched
off the router's resolved intent).
- 8.2 `WikiRepo.writeFoodEntry` + structured frontmatter.
- 8.3 `FoodHazardScreener` + `assets/hazards/food_toxins.yaml` +
`assets/hazards/escalation.yaml`.
- 8.4 capture-flow wiring (Feeding tile on `_QuickCaptureRow`,
before/after toggle, router invocation from
`photo_capture_screen.dart`, `PhotoCapturePrefill.mealPhase`
extension).
- 8.5 phase wrap + on-device verification.
https://claude.ai/code/session_019ZiQQgMegczk4jZm4Ef3pB
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
First implementation PR of the Phase 8 feeding restructure that
shipped in PR #5. Ships the harness primitive that Phases 8.1, 8.3,
and 8.4 all plug into.
Pure dispatcher. No UI. No
WikiRepochanges. Noagent_loopwiring.
What landed
lib/harness/intake/intent_router.dartIntakeIntentflat enum +IntakeIntentRouterclasslib/app/providers.dartintakeIntentRouterProviderwired againsthaikuLlmClientProvidertest/harness/intake/intent_router_test.dartROADMAP.md[ ]→[x]DECISIONS.md5 files, +504 / −1.
Hybrid resolution contract (DECISIONS rows 98 + 104)
explicitHint != null→ returned immediately. No LLM call,no gate check. The caller (Phase 8.4 form toggle) already knew
the intent.
explicitHint == null→VisionGate.check()→ Haikuclassification on the photo (+ optional caption) → JSON parse →
enum value or
generalMemoryfallback.Always-safe contract. Every failure mode degrades to
IntakeIntent.generalMemory: gate block, LLM throw, 8s timeout,malformed JSON, missing
intentfield, JSON decoded to a non-object,unknown drift string (
"snack","logmeal_after"). The captureflow never has to handle a null and never has to surface an error
toast.
generalMemorypreserves the user's ability to log a memoryregardless of classifier health.
In-flight design correction (worth flagging)
The approved plan claimed "Haiku as classifier was considered +
deferred — would require transport per-call model override, which
the codebase doesn't have today." That claim was wrong.
Reading
lib/app/providers.dartduring implementation surfacedhaikuLlmClientProvideratproviders.dart:279— wired by Phase 6task 6.8 per DECISIONS row 41 (f) ("Sonnet for the extractor, Haiku
for [lightweight calls]"). The router is exactly the
lightweight-call use case the precedent locks. Implementation uses
Haiku via that existing seam (same pattern as
affectiveObserverProviderat line 287). Same router code; justa better DI choice. DECISIONS row 104 captures the corrected
rationale instead of carrying the plan's wrong claim forward.
The plan also speculated a separate
intent_router_providers.dartfile. The codebase clusters all providers in
lib/app/providers.dart(single hub pattern); the new provider lives there now, sibling to
photoExtractorProvider.Design lock (DECISIONS row 104)
IntakeIntentenum +fromIdOrFallbackfactory —mirrors
PhotoSetting/PhotoActivityatphoto_extractor.dart:159-190. Three cases is below thethreshold where a sealed hierarchy earns its abstraction cost.
Future lenses add cases (e.g.
logGroomingAfter) withoutrestructuring callers.
IntakeIntent? explicitHintparameter type (notbool? isBefore, notMealPhase?) — symmetry between inputand output, lens-extensible without growing another optional
parameter per lens.
Future<IntakeIntent>return +generalMemoryfallback on every failure path — the router is the always-safe
dispatcher.
PhotoExtractor's 15s — runs upstreamof the extractor in the same form-preview window, must fit
underneath).
intakeIntentRouterProviderwatcheshaikuLlmClientProvider,same pattern as
affectiveObserverProvider. The classifiercall is a 3-class, short-prompt task — Haiku's profile fits.
VisionGaterouting even though intake is FREE per row 102— keeps the entitlement/BYOK/quota path uniform with the food
extractor (8.1) and the rest of the photo intake stack.
prefer
logMealAfter"; "when unsure between any meal intent andnon-meal prefer
generalMemory". Deliberate "soft cases getsofter" amortization. Hazard gate (8.3) still fires on either
meal intent.
Cost trade-off (deliberate accept)
Non-hinted food photos fire 2 vision calls: Haiku classification +
Sonnet extraction. Intake is free per row 102, so the user pays
nothing; the 8.4 form UI defaults to surfacing the before/after
toggle so the expected real-user path ships an explicit hint and
short-circuits the classifier entirely. Worst case: 1 Haiku + 1
Sonnet. Expected case: 1 Sonnet.
Test plan
flutter analyze --fatal-infos; echo "exit=$?"→exit=0,"No issues found! (ran in 3.3s)".
flutter test --reporter expanded; echo "exit=$?"→exit=0,1776 tests pass (baseline 1756 + 20 new router tests).
flutter test test/harness/intake/intent_router_test.dart→ 20/20 in <1s.
The 20 router tests cover:
Explicit hint short-circuit parameterized across all 3 enum
values (LLM never called when hint present).
Soft-case → intent mapping for each of the 3 intents.
Prompt + ImageBlock anchors in the LLM call.
Caption threading — present caption appears as
TextBlock('Caption: ...'); whitespace-only caption is NOTthreaded.
Code-fence stripping — model wrapping
```json ... ```still parses.
Always-safe fallback for every failure path: malformed JSON,
missing field, drift string, non-object JSON, transport throw,
timeout, gate block.
fromIdOrFallbackdrift tolerance tested directly (knownnames; null; unknown / drifted strings; empty string).
On-device — N/A for this PR. Pure harness primitive, no
UI. The router gets exercised end-to-end on a device in
Phase 8.4's on-device gate.
Out of scope (subsequent Phase 8 PRs)
WikiRepo.writeFoodEntry+ structured food frontmatter (MealPhaseenum lands here)FoodHazardScreener+assets/hazards/food_toxins.yaml+assets/hazards/escalation.yamlphoto_capture_screen.dart,PhotoCapturePrefill.mealPhaseNext session
After merge: planning the smallest follow-on task. Most likely
Phase 8.1 (food-mode extractor) — it's the second harness
primitive that 8.3 (hazard gate) screens the output of, and like
8.0 it's pure unit-testable with a fake
LlmClient. Alternative:8.2 (storage schema) if you'd prefer to land the frontmatter
contract before another LLM-shaped PR.
https://claude.ai/code/session_019ZiQQgMegczk4jZm4Ef3pB
Generated by Claude Code