Skip to main content

Architecture

In one line: HIGHFIELD is one Lua agent assembled like an assembly line — inbound message → preprocessors (clean & label) → the LLM (Alex Carter, with a persona, skills, and tools) → postprocessors (tidy & police) → outbound — with scheduled jobs running on timers and Business Central as the filing cabinet.

What it is

Think of HIGHFIELD as an assembly line. A message comes in (email or chat). Before the AI sees it, a row of small pre-processors clean and label it: Is this an emergency? Who is the sender? Which ticket is this about? The AI — Alex Carter, equipped with a fixed personality and a toolbox — reads the prepared message, decides what to do, and calls tools that read and write Business Central. After the AI writes its reply, a row of post-processors tidy and police the outgoing message. Separately, scheduled jobs wake up on a timer to chase stuck tickets and send reports. Business Central is the filing cabinet; the email service is the post office.

How it works — the Lua agent model, piece by piece

HIGHFIELD is one LuaAgent object, assembled in src/index.ts:68-632, made of seven kinds of building block:

  • Persona — the agent's fixed instructions and personality: who Alex Carter is, the rules it must follow, the language it must never use. A long instruction block authored directly in src/index.ts:71-546.
  • Skills — bundles of tools grouped by audience. Three: maintenanceSkill (tenant + agent-side lifecycle tools), vendorSkill (contractor self-service), adminSkill (manager/approver), wired at src/index.ts:549. A skill also carries plain-English guidance on when to use its tools.
  • Tools — the individual actions the AI can take, each a small TypeScript class with a typed input. They are where reads and writes to Business Central happen. Under src/tools/{intake,vendor,approval,completion,escalation,admin}. See APIs, webhooks & tools.
  • Preprocessors — code that runs on every inbound message before the AI, in a fixed priority order (lower first). See the pipeline detail.
  • Postprocessors — code that runs on the AI's outgoing reply, in array order — the safety net that keeps the agent honest and on-brand.
  • Jobs — five scheduled background tasks (src/index.ts:570) that run with no human in the loop. See Scheduled jobs.
  • Webhooks — 11 HTTP endpoints for outside systems and email (src/index.ts:552-564). The most important is inbound-email. See APIs, webhooks & tools.

There is also a batching config (src/index.ts:597-602): rapid-fire follow-up messages within an 8-second window are coalesced into one coherent turn instead of producing several contradictory replies. A lone message is never delayed (firstMessageDelayMs: 0).

Under the hood — persona source of truth: the persona is authored in src/index.ts, not in the generated lua.skill.yaml (which is regenerated/overwritten on push). Editing the yaml has no effect — edit src/index.ts.

Key external systems

  • Microsoft Dynamics 365 Business Central (BC) — the system of record. Tickets, customers (tenants), vendors, quotes, purchase orders, and audit events all live here. The agent reaches BC two ways (README.md:1-4,34): the standard API v2.0 (tickets stored as JSON attachments on customer records — no special BC setup) and a custom per-tenant extension API (native BC pages and reporting). This is a deliberate dual-write pattern. HTTP plumbing — OAuth token, retries on 429/5xx, OData query building, multi-company support — lives in src/services/bc-client.ts:18-22,54-86. A friendlier wrapper, BCDataAdapter (src/services/bc-data-adapter.ts:5-9,30-38), makes tickets look like ordinary records to tool code even though they're stored as attachments. The live company is "Tribeca" (TRIBECA_COMPANY_ID).
  • Email channels — the primary live channel. There are two outbound lanes (see Communication & email): the production Lua send-message API and a separate AgentMail client. Inbound mail arrives at the inbound-email webhook or the native Lua email channel (preprocessors).
  • CDN / image storage — tenant and vendor photos are uploaded to cdn.heylua.ai and referenced by URL; the agent attaches those URLs to BC tickets. (Re-hosting matters: raw S3 attachment URLs expire and poison history — see Operations.)

Tech stack

LayerChoiceSource
LanguageTypeScripttsconfig.json, all of src/
Platform / frameworkLua agent platform via lua-cliimport { LuaAgent } from 'lua-cli' (src/index.ts:1)
LLM modelanthropic/claude-sonnet-4-6src/index.ts:567
System of recordDynamics 365 Business Central API v2.0 + custom extensionsrc/services/bc-client.ts, bc-custom-api.ts
HTTP clientaxiossrc/services/bc-client.ts:6

⚠️ Unverified / drift: an LLM_FAILOVER_MODEL env lever (Sonnet → Gemini) exists in the deployed build but not in the current branch's src/ — the working tree hardcodes the model at src/index.ts:567. See Operations.

Gotchas & failure modes

  • Stale barrel files. src/preprocessors/index.ts and src/postprocessors/index.ts are not the registration points — src/index.ts imports each file directly. The barrels re-export only a subset and are misleading.
  • Array order ≠ priority order. The preprocessor array in src/index.ts:584 is not in priority order; the priority field on each preprocessor is the source of truth.
  • Admins skip most of the pipeline. Once user.isAdmin is set (priority 15), several later preprocessors skip — admins route straight to the LLM with the admin skill. This deliberately fixed a ~60s no-response incident.