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 atsrc/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 isinbound-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 generatedlua.skill.yaml(which is regenerated/overwritten on push). Editing the yaml has no effect — editsrc/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 insrc/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-emailwebhook or the native Lua email channel (preprocessors). - CDN / image storage — tenant and vendor photos are uploaded to
cdn.heylua.aiand 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
| Layer | Choice | Source |
|---|---|---|
| Language | TypeScript | tsconfig.json, all of src/ |
| Platform / framework | Lua agent platform via lua-cli | import { LuaAgent } from 'lua-cli' (src/index.ts:1) |
| LLM model | anthropic/claude-sonnet-4-6 | src/index.ts:567 |
| System of record | Dynamics 365 Business Central API v2.0 + custom extension | src/services/bc-client.ts, bc-custom-api.ts |
| HTTP client | axios | src/services/bc-client.ts:6 |
⚠️ Unverified / drift: an
LLM_FAILOVER_MODELenv lever (Sonnet → Gemini) exists in the deployed build but not in the current branch'ssrc/— the working tree hardcodes the model atsrc/index.ts:567. See Operations.
Gotchas & failure modes
- Stale barrel files.
src/preprocessors/index.tsandsrc/postprocessors/index.tsare not the registration points —src/index.tsimports each file directly. The barrels re-export only a subset and are misleading. - Array order ≠ priority order. The preprocessor array in
src/index.ts:584is not in priority order; thepriorityfield on each preprocessor is the source of truth. - Admins skip most of the pipeline. Once
user.isAdminis 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.