Beaux TaylorWork · Meridian, Idaho

Work › Health plan › Engine design

Health plan, stage one · the autonomous operator

Engine design

The design note for "the engine": the one door to the AI, the proposal lifecycle, the autonomy tiers, and the things no tier can ever grant. I wrote it and built the code from it; the rules in it are the ones Beaux set.

The engine does work automatically and puts finished proposals (with evidence) in the founder's queue; he approves/edits/rejects with one tap. Every action + decision is recorded in engine_actions — that record is the training data for earning autonomy. Module builders: import from these two files only; only lib/engine/ai.ts may import @anthropic-ai/sdk, and it is server-side only (never import in a client component).

lib/engine/ai.ts — the only door to the AI

Model: process.env.ENGINE_MODEL (falls back to claude-opus-4-8). Key: process.env.ANTHROPIC_API_KEY (root .env). No temperature/top_p — this model family rejects them.

aiAvailable(): boolean

Key present? Check this FIRST; when false, run your deterministic fallback (keyword match, price book) and record confidence: "low" — never throw a missing-key error at a caller that could have degraded gracefully.

extractJson<T>(opts: {
  task: string;        // short name for logs/rows — never PHI
  system: string;
  prompt: string;      // PHI-minimized: care-need text/category/CPT/age-band/sex/urgency ONLY
  schema: object;      // JSON Schema: additionalProperties:false + required on every object
  maxTokens?: number;  // default 4000
}): Promise<{ data: T; modelUsed: string; latencyMs: number }>

Structured output via output_config.format (json_schema); parses the first text block with JSON.parse. Store modelUsed/latencyMs on the engine_actions row via propose().

readDocument<T>(opts: extractJson opts & {
  media: { mediaType: string; base64: string };  // no "data:" prefix
}): Promise<{ data: T; modelUsed: string; latencyMs: number }>

Same, but the model also sees a file: application/pdf → document block; image/jpeg|png|gif|webp → image block. File first, instructions second.

class EngineAiError extends Error {
  kind: "no_api_key" | "rate_limited" | "connection" | "api_rejected" | "bad_response";
  retryable: boolean;  // true → try again later unchanged
}

Every failure path throws this, with a plain-language message safe to show the founder. SDK errors are caught most-specific-first (RateLimitError → APIConnectionError → APIError). Logs carry task/latency/tokens only — never prompt contents (no PHI in logs). Callers must NOT log prompts either.

PHI rule (hard): never send name, date of birth, member code, access code, or address to the API. Ranking rule (hard): the AI drafts prose and extracts data; it never ranks options and never decides emergencies (triage is a hard-coded rule list, lib/engine/triage.ts, not built yet).

lib/engine/actions.ts — the proposal lifecycle

type EngineActionType = "triage" | "intake" | "price_research" | "options_draft"
  | "order_confirmation_draft" | "bill_parse" | "bill_response_draft"
  | "member_message_draft";
type AutonomyTier = "auto" | "approve_each" | "draft_only";
type EngineConfidence = "high" | "medium" | "low";
getTier(actionType): Promise<AutonomyTier>

Reads autonomy_settings; falls back to DEFAULT_TIERS (same values as the seed) if the row is missing.

propose(actionType, {
  careRequestId?, billCaseId?,   // link to what the work is about
  proposal, evidence,            // any JSON-able values; stored as text
  confidence,                    // "low" for no-AI fallbacks
  modelUsed?, latencyMs?, errorNote?,
}): Promise<EngineAction>

Records finished engine work. Tier auto → row status auto_done (acted + recorded, visible); otherwise proposed (founder's queue). First marks any older still-proposed rows of the same action type on the same care request / bill case as superseded — the founder only sees the latest.

decide(id, "approved" | "rejected", finalJson?): Promise<EngineAction>

Founder's one-tap. Approve as-is → approved (final_json = proposal); approve with a changed finalJson → edited (final_json = what was used); reject → rejected. Atomic claim: only a proposed row can be decided; repeats return the row unchanged.

listProposed(): Promise<EngineAction[]>       // founder queue, oldest first
getForRequest(careRequestId): Promise<EngineAction[]>  // audit trail, newest first

Autonomy tiers (seeded defaults — npm run seed)

action_type tier why
triage auto pure analysis; rules decide, never the LLM, never cost-aware
intake auto parsing the member's words — reversible
price_research auto lookups only — reversible
bill_parse auto reading an uploaded bill — reversible
options_draft approve_each publishing options commits the plan
order_confirmation_draft approve_each script for a call the founder makes
bill_response_draft approve_each letter that leaves the company
member_message_draft approve_each reaches a member

Never automated, no tier can grant it: money movement, anything that narrows a member's options, anything sent outside the company (the founder fires all outward communications), and there is no negotiation capability anywhere. If uncertain, uncertainty is priced to the plan, never the member.

Tables (prisma/schema.prisma)

  • engine_actions — one row per engine action: action_type, proposal_json, evidence_json, confidence, status (proposed | approved | edited | rejected | auto_done | superseded), final_json, decided_at, model_used, latency_ms, error_note, nullable FKs care_request_id / bill_defense_case_id.

  • autonomy_settings — action_type (unique) → tier.