Skip to content

Event taxonomy

The full AgentEvent discriminated union from lib/types.ts. Every event has a type and an optional data payload.

{
type: "record_project_facts",
data: {
projectValue?: number;
depositAmount?: number;
depositPct?: number;
productSpec?: "panels_only" | "panels_battery" | "panels_battery_ev" | "panels_battery_ev_other";
systemKwp?: number;
batteryKwh?: number;
retailerName?: string;
}
}

Effect: merges into case.project. Computes deposit % from amount and vice versa.

{
type: "record_customer_contact",
data: {
firstName?: string;
lastName?: string;
email?: string;
mobile?: string;
}
}

Effect: merges into case.contact.

{ type: "generate_customer_link", data: {} }

Effect: sets case.status = "awaiting_customer".

{ type: "installer_handoff_complete", data: {} }

Effect: sets case.installerHandoffComplete = true, case.status = "awaiting_customer".

{
type: "record_eligibility",
data: {
isOver18: boolean;
isUkResident: boolean;
isHomeowner: boolean;
isEmployed: boolean;
}
}

Effect: stores eligibility, sets status to quote_ready (all yes) or ineligible (any no).

{ type: "record_provisional_quote", data: { termMonths: number } }

Effect: computes the indicative quote at the prime rate via getIndicativeQuote().

{
type: "record_email_preference",
data: { wantsEmailCopy: boolean; email?: string }
}

Effect: stores email opt-in choice.

{
type: "record_personal_facts",
data: {
fullName?: string;
dateOfBirth?: string;
postcode?: string;
addressLine1?: string;
addressLine2?: string;
town?: string;
county?: string;
propertyType?: string;
residentialStatus?: string;
}
}

Effect: merges personal facts. Transitions status to customer_active if needed.

{
type: "record_financial_facts",
data: {
employmentStatus?: string;
annualIncome?: number;
monthlyOutgoings?: number;
existingCommitments?: string;
}
}

Effect: merges financial facts.

{
type: "record_vulnerability_indicators",
data: { indicators: string[]; note?: string }
}

Effect: stores Consumer Duty flags + free-text note in case.financial.

{
type: "present_disclosure",
data: { id: "service_status" | "credit_search_consent" | "pre_contract_summary" }
}

Effect: records that a disclosure has been shown to the customer with a timestamp.

{ type: "acknowledge_disclosure", data: { id: string } }

Effect: records the customer’s I-understand / I-confirm response with a timestamp.

{
type: "capture_consent",
data: {
consentType: "credit_search" | "data_sharing" | "marketing";
granted: boolean;
}
}

Effect: records explicit consent. Replaces any prior consent of the same consentType (the store filters out the older record before pushing the new one). Triggers reconciliation rules R1 and R2 when consentType === "credit_search" and granted === true (auto-ack the linked disclosure, auto-present pre-contract). The demo only emits credit_search; the other two values exist in the type union for future expansion.

{ type: "submit_application", data: {} }

Effect: sets status to submitting. The route handler runs the waterfall.

{ type: "accept_counter_offer", data: {} }

Effect: only fires if caseState.waterfall.awaitingCounterDecision is true. Calls acceptCounter(waterfall) to mark the latest counter-offer step as accepted, sets case.selectedOfferId from waterfall.acceptedOffer.id, and transitions status to selected.

{ type: "refuse_counter_offer", data: {} }

Effect: clears waterfall.awaitingCounterDecision, sets status = "waterfall_running". The route handler then re-runs runWaterfall with fromIndex = waterfall.steps.length, picking up at the next lender on the panel.

{ type: "select_offer", data: { offerId: string } }

Effect: legacy parallel-offers flow. Marks an offer as selected.

{ type: "withdraw", data: { reason?: string } }

Effect: sets case.status = "withdrawn", records case.caseOutcome with the reason.

{
type: "case_outcome",
data: {
kind: "selected" | "declined" | "ineligible" | "withdrawn" | "completed";
reason?: string;
}
}

Effect: records the terminal case outcome.

{ type: "case_complete", data: {} }

Effect: a no-op if the case is already in declined, ineligible, or withdrawn. Otherwise sets status = "complete" and, if no caseOutcome is set, records a fallback { kind: "completed", recordedAt }. The agent emits this at the end of any path that didn’t already lock to a more specific terminal state.

Every event in this taxonomy can be emitted by either:

  • The model: as inline <agent-event> tags in its prose
  • The UI: as directEvents in the chat route’s request body

Both go through the same applyEvents function. The UI uses direct events for regulated commitments (eligibility answers, consent, application details, vulnerability flag). The model emits events for narrative pacing (greeting, signposting which disclosure to present next).

See Event protocol for parsing details.