previews · Architecture & design · The loop, in TypeScript

Lawrence engineering · Strategic plan · June 2026

The loop, in TypeScript

The Lawrence agent runtime runs in TypeScript on the Vercel AI SDK. v3 layers apps → apis → services, and an agent loop is api-tier work, so the runtime is an api: the loop lives in the chat api and its tools call the matter, content and search services through the shared @lawhive/content-framework, the same path lawrence-api already uses. Every tool is TypeScript and lawrence-api stays the data authority. Capability 02 already rewrites this loop, so the move is largely a choice of language — and this doc also settles where durable work and code execution run under UK/EU residency.

agents repo · not Eve straight port · api-tier status: draft for review

Post-review iteration (29 Jun 2026). This drew detailed feedback — fluent builder, declarative registry, deny-by-default VFS, model-from-config, layout. It's folded into a proposal: defineAgent, reshaped (current vs proposed, side by side).

Update · AI SDK v7 is the base (GA 2026-06-25). This doc was written against v6; v7 is now the target, and a breaking major. Two things change the plan, not the direction. (1) Agents are first-class — ToolLoopAgent, WorkflowAgent, and a HarnessAgent (which can even run Pi / Claude Code / Codex) — and several pass-2 items we'd deferred now ship natively: durable/resumable runs, tool approvals (HITL, HMAC-signed), and typed tool context (which maps onto our initialContext hook). (2) Durable runs are now self-hostable in eu-west-2 with zero US residency: WorkflowAgent backed by @workflow/world-postgres (our own Postgres + an ECS worker), which re-opens the "keep Inngest" call below — left as an open question pending a small spike. The live v3 chat loop is Python (lawrence-api proxies to the agent-gateway), so bumping TS ai can't break the agents' reasoning; the real build-time gates (ESM-only, the ai version pin) are in Open questions.

First, the primitives

Vercel frames its agent tooling as a three-layer "agent stack" (their post). Here's the whole board, with what each piece is and whether it's in scope for this plan. The rest of the doc only goes deep on the four that touch our decision.

1 · Model connectivity
AI SDKOne TypeScript interface to any model — streaming, tool calls, structured output, an agentic loop with stop conditions.the runtime target
AI Gateway"A CDN for tokens": one endpoint to hundreds of models with failover, cost tracking, and bring-your-own-key.evaluate as model edge
2 · Workflow execution
Workflow (SDK)Durable execution — checkpoint every step, resume after a crash or deploy, sleep for months, wait on human input.managed is US-only
SandboxIsolated Firecracker microVMs — full Linux, filesystem, Docker; credentials injected per task, not baked in.US-only → AgentCore instead
Queues + Fluid ComputeThe substrate beneath the above: a region-pinnable message bus and long-running streaming functions.
3 · Data & tool integration
ConnectAgents act on Slack / GitHub / Salesforce / Notion via short-lived, scoped, per-task tokens — auditable user→agent→service.later, for outbound
Chat SDKShip one agent across 15+ channels from a single codebase, keeping context across surfaces.
eve— the opinionated framework on top: agents as files (a model spec + markdown instructions + TypeScript tools + skills + sub-agents + channels + schedules). It bundles all of the above as fully-managed infra. We're explicitly not adopting eve — we take the SDK layer (the AI SDK) and self-host on AWS, which is what the rest of this doc works out.

The loop is already being rewritten

Capability 02 — the framework refactor in One loop, many Lawrences — is a from-scratch rewrite of the loop. ChatAgent.generate_stream() is ~780 lines welded to an HTTP request, and the plan tears it down to BaseAgent + hooks. So the rewrite cost is committed; the only open question is which language to rewrite it in.

Rewriting the loop in TypeScript rather than Python is a marginal delta on work that is already committed — and it unlocks a structural win.

Two things make TypeScript the high-leverage version of that same work:

This doc takes the decision already made — not Eve, no fully-managed infra, only the agent runtime moves, lawrence-engine and ingestion stay Python — and works out exactly what that costs, tool by tool, plus where the Vercel primitives (Sandbox, Workflow, AI Gateway) actually fit once you check where the data lands.

The shape: a straight port, in the api tier

v3 layers apps → apis → services, and an agent loop calling tools is api-tier work — so the runtime is an api, not a new layer. It reuses the shared @lawhive/content-framework + service clients (already TS) to call services directly. content-retrieval and kms-utils are transport to lawrence-api, not a data/crypto layer, so every tool ports the same way. Two placements fit (see the topology diagrams): the loop in the chat api (B-fit-1, recommended), or a sibling agent-runtime api.

AWS eu-west-2 · self-hosted · UK/EU residency APPS APIS SERVICES legal-os Lawrence chat api · TypeScript agent loop (AI SDK) + tools via @lawhive/content-framework owns AIThread + stream · Python agent-gateway deleted domain services matter · content · messaging · email assignment · work · identity shared content-framework adapters Inngest events · durable background / triggers AI Gateway model edge (optional) useChat service clients models events → wake
The runtime is an api: the loop lives in the chat api and its tools call the domain services through the shared @lawhive/content-framework — the same way lawrence-api already does — a normal in-tier call, in a single language, with every edge pointing down. Inngest orchestrates durable/background work over events; AI Gateway is an optional model edge. This is B-fit-1, the loop in the chat api; B-fit-2 runs it as a sibling api instead — see the topology diagrams.

▤ Runtime topology — current vs proposed (diagrams) →
The verified current call path and the two ways to move the runtime to TS without the cycle, drawn out.

What it costs, tool by tool

Eighteen live tools (four dead packages — edit, read, faq, legal-search — already absorbed into platform-vfs or retired). The tool code is mostly thin glue; the libraries a tool seems to “drag in” turn out to be transport to lawrence-api and the services, not substance.

18
live tools to migrate
18
port to TypeScript
2
stay Python (engine + ingestion)
~8.4k
tool LOC — thin glue over services
SML port effort stream yields progress deltas cite carries citation config PII reads/writes matter client data

Pure-logic, LLM & public-API 13 tools

Pure-logic, LLM, or public-API — no shared-data coupling. They port directly and keep native streaming via async-generator execute.

Sintent-classification
Splanning-tools
Sload-skill
Sdecompose-query
Sreformulate-query
Sretrieve-documents
Slegal-questionstream
Svector-searchcitePII
Sweb-searchcite
Scheck-case-status
Sfill-formPII
Mcase-lawcite
Mlegislationcite

Matter-data tools 5 tools

These read and write matter data. In TS they call the matter / content / search services directly through @lawhive/content-framework — the same path lawrence-api uses — and the tools decrypt nothing, since the services return resolved content.

Mdocument-questionstreamcitePII
Mmatter-infostreamcitePII
Mmatter-messagesstreamcitePII
LsearchPII
Lplatform-vfscitePII

The two L tools (search 1,559 LOC + platform-vfs 3,228 LOC) are 57% of all tool code.

The dependency gravity is the real story

What each shared library a tool depends on becomes in TypeScript. The two biggest are transport — client stubs to lawrence-api and the services — so they're deleted, not reimplemented:

Shared libraryVerdictWhat it costsTools affected
lawhive-content-retrievaldeleteNot a data layer — client stubs to lawrence-api (v3) plus the matter / content / search services and S3. No crypto. In-tier the tools call those services directly via the shared framework, so the package goes away.document-question, matter-info, matter-messages, search, platform-vfs
kms-utilsevaporatesService-to-service request signing, not data crypto — and it already no-ops on local URLs. In-zone it's unneeded; the only residual is the hop to the ingestion search-service. Not a keystone.was: document-question, legal-question, search, vector-search, platform-vfs
legal-sourcesport (watch)4 public-API clients (easy) + lxml AKN/Atom namespaced XML parsing (no clean TS analogue — the watch-item).case-law, check-case-status, legislation
citation-modelsportzod equivalent of the reference/passage models + the ❦…❧ citation parser in the loop.8 tools + the loop
ai-stream / tool-definitionport (first)The wire protocol + base tool contract. Largely subsumed by AI SDK tool() + streamText. Must land before any tool ships.all

The matter-data tools reuse @lawhive/content-framework + the service clients, which already exist in TS — so they're a straight port, not a rewrite of the data layer.

The migration's real cost is the loop itself plus the genuinely-new TS pieces: the ai-stream wire contract, the citation parser, and legal-sources' AKN/XML. The runtime calls services the way lawrence-api already does, so content-retrieval and kms-utils don't carry over — there is no Python data layer.

Where MCP fits

The runtime reaches our own matter data in-tier, through @lawhive/content-framework — a normal service call, not a cross-language hop. MCP is for a different job: external / third-party tools (Gmail, court portals, Slack via Vercel Connect), where a scoped, per-task token and an auditable user→agent→service trail are the point. Use MCP for outbound integrations; talk to our own services directly.

Code execution: keep the compute in-region

Capability 06 lets the agent run code in a sandbox over matter files. That code processes client PII, so where it runs is a compliance gate, not a latency choice.

The pattern: stage working copies of matter files into a sandbox, let the agent run code that transforms or assembles them in one pass, return the result. (Court bundling is the first driver; the capability is general.) Vercel Sandbox is the obvious primitive — except for where it runs.

Confirmed: Vercel Sandbox is US-only

Vercel Sandbox provisions only in iad1 (US-East / N. Virginia). Sandbox.create() has no region parameter (verified full param list); sandbox.region is a read-only output. The docs state verbatim: "Currently, Vercel Sandbox is only available in the iad1 region." It is persistent-by-default. So staging matter files into it is a cross-border transfer of personal data to the US, not a latency choice. Vercel offers lawful-transfer mechanisms (signed DPA, EU SCCs + UK Addendum, DPF, SOC 2/ISO) but no product-level EU/UK residency guarantee for Sandbox — the concepts page itself punts residency to "your compliance team." An EU region was requested (Feb 2026) but is unscheduled.

The design is runtime-independent: the agent passes file refs, never bytes or credentials; lawrence-api stages a decrypted, curated subset and holds the keys; a vetted library does the deterministic work; output returns through a preview-then-confirm step; egress is locked and the sandbox is non-persistent. Only the microVM swaps.

So the runtime is AWS Bedrock AgentCore in eu-west-2, inside Lawhive's own AWS account — the same boundary where lawrence-engine and ingestion already live. (Confirm current eu-west-2 availability before committing.) Since v3 serves both US and UK traffic, route the compute by the matter's residency — UK matters to eu-west-2, US matters to us-east-1 — under one multi-region AgentCore API; the runtime-independent design makes that a routing config, not two builds.

Code execution is the most expensive way to get a result — it earns its place only when the work is genuinely open-ended.

It is demand-sequenced and last on the capability ladder. Where a fixed in-region engine call serves the need, prefer that; stand up the sandbox when a capability genuinely needs agent-written code — in eu-west-2, not iad1.

Durable execution: keep Inngest

Two independent disqualifiers rule Vercel Workflow out for the agent loop, either one sufficient on its own. The headline event price is irrelevant.

DimensionInngest Cloud + our execution (current)Vercel Workflow (managed)
Execution modelSplit — the cloud orchestrates (queue, schedule, retry, durable state); your function/step code runs on your servers via Connect websocket / HTTP serve. You own the compute.Not split — each step compiles to a Vercel Function route and runs on Vercel. The managed product has no "your compute" mode; the only self-run path is the self-hosted Workflow SDK.
Where the data landsMixed — execution in-region (eu-west-2), but durable step state + event payloads persist in Inngest Cloud (US). Already your exposure today.US — managed persistence (all state + event logs) pinned to iad1, and compute moves to the US too. Strictly more US-resident than Inngest.
Latency for a tool loopBuilt for it — keep the loop in one coarse step; sleeps/waits aren't billed or countedStructural tax — each LLM call / tool exec is a step; ~4–5s queue round-trip/step, grows with step count; a 10–20 step turn becomes minutes (sourced to vercel/workflow issues — treat as hypothesis)
Billing unitN+1 executions/run · Pro $75/mo incl 1M, $50/1M over~3N events/step · $0.02/1K ($20/1M) plus unbundled Functions compute plus Queues ops
Cost shapebundled; cheap if the loop stays coarse-grainedheadline events cheap (~$27/mo illustrative) but compute + queue ops + latency tax are the real bite

So is Workflow the same orchestration-only model as Inngest? No. With Inngest you run the execution and the cloud orchestrates; with managed Workflow, Vercel runs your step code on its own Functions. The only way to keep compute on our servers under the Workflow umbrella is the self-hosted Workflow SDK (Postgres World) — which also means owning the Postgres durable backend, i.e. fully self-hosting, with no edge over self-hosting Inngest. Either way the TS runtime enqueues and awaits Inngest functions via the Inngest TS SDK, so keeping Inngest is fully compatible with the TS loop.

The honest residency picture, now that we've confirmed we're on Inngest Cloud: durable step state already lands in the US today, and in the managed tier of both products that's unavoidable. So "keep Inngest" wins not because it's fully EU, but because it keeps execution on our servers, adds zero migration, and doesn't also move compute to the US the way managed Workflow would. Two levers narrow the exposure without leaving Inngest: keep the agent's LLM/tool loop in one coarse step — or outside durable steps entirely, using Inngest only to trigger/background — so PII-bearing loop internals are never checkpointed to Inngest Cloud (the same lever that controls cost); and if a hard EU-residency line is required, self-host Inngest (single binary, Postgres + Redis, eu-west-2), which removes the US state entirely. Workflow would only win if Vercel both ships EU persistence with a contractual guarantee and eliminates the per-step replay overhead. Neither is true today.

What v7 changes here (June 2026). When this was written, the self-hosted Workflow SDK (Postgres World) had "no edge over self-hosting Inngest." v7 gives it one: WorkflowAgent (@ai-sdk/workflow) is built to run on that runtime, so backing it with @workflow/world-postgres (own Postgres + an ECS Fargate worker; graphile-worker; Postgres NOTIFY/LISTEN; zero Vercel callout) yields durable runs, restart-surviving approvals and stream persistence in eu-west-2 — the pass-2 durable-streams capability, native and in-region. There is no viable Inngest "World" adapter (the interface mismatch makes building one a bad bet), so the call is cleanly either/or: WorkflowAgent + Postgres World vs keep Inngest and compose ToolLoopAgent inside a step.run (reuses today's pattern, but we re-implement approvals / replay / stream-persistence ourselves). Left open pending a spike; either way Inngest stays for non-agent background jobs.

Where the data lands — the residency map

The whole Vercel-primitives question reduces to one table. EU/UK regions exist for Functions and Queues; they do not exist for Sandbox or managed Workflow persistence.

PrimitiveEU/UK region?For our use
Vercel SandboxNo — iad1 onlyCode execution over matter files would put PII in the US. Use AgentCore eu-west-2 instead.
Managed Vercel WorkflowNo — iad1 onlyPersistence holds the loop's PII step I/O in the US. Ruled out. Keep Inngest.
Workflow SDK (Postgres World)Yes — self-hostEU-resident but full self-managed ops; no edge over self-hosted Inngest.
Vercel QueuesRegion-selectablelhr1/fra1 available, but "strict residency not yet supported" (failover may relocate).
Vercel FunctionsYesPin to lhr1 (Pro = 5 regions). Not where we host anyway (AWS).
AI GatewayUnverifiedAdopt as the model edge via createOpenAICompatible; if the gateway data-path residency is unconfirmed, point it at a self-hosted in-region proxy.

Where we'd start: a matter-opening agent

Don't migrate Lawrence's chat first. Prove the platform on one small agent that lives off Lawrence's critical path — and that's a draft nobody minds reviewing.

The first agent on the TS runtime isn't Lawrence — it's a matter-intake agent. An admin finishes an intake call, drops in the documents and a couple of notes, and the agent opens a matter pre-populated with a title and summary, custom fields, key dates and tasks, in OPENING status for the admin to confirm. It's the deliberate opposite of Lawrence: firm-scoped (it creates the matter, so no matterId goes in), admin-principal, a handful of write tools, and a structured output instead of free-form chat.

That's exactly why it's the right trial — it exercises the whole generalised-agent envelope at once (scope, principal, a reduced tools() set, structured output), and the only new infrastructure it forces is a single matter:// create tool; key dates, tasks, notes and custom fields the platform already exposes. Bounded blast radius, real value, off the chat path.

Intake is the proof; the platform is the point. We learn the slug / principal / scope envelope on a job nobody enjoys doing by hand, then reuse it for the rest of "many Lawrences."

The full trial — the agent spec, the v3 matter-model grounding, and the one capability to build — is in the matter-intake trial companion.

The sequence

With the platform proven by the intake agent, the broader loop migration follows. Rough order, not a calendar; each stage is independently shippable. Foundations (the wire contract + citation parser) come before any tool ships.

stage0

Foundations — nothing ships to users

Port ai-stream/tool-definition onto AI SDK tool() + streamText with a golden-fixture conformance test against the real Python emitter. Port citation-models to zod + the ❦…❧ parser into the loop. (Signing only matters for a cross-trust-zone hop like the ingestion search-service; in-tier calls need none.) Loop runs with zero tools, streams text.

stage1

Runtime shell + cheapest tools

Stand up the streamText loop (stopWhen, prepareStep, gateway routing); wire it to lawrence-api. Port the pure-logic / LLM tools first — intent, planning, load-skill, decompose, reformulate, retrieve-documents. Prove the async-generator streaming path end-to-end.

stage2

KMS + public-API tools

legal-question, vector-search; web-search (exa-js directly); case-law, check-case-status, legislation (legal-sources port — AKN XML parity is the watch-item, fall back to a Python parser microservice only if fixtures slip); fill-form (Inngest TS SDK). 13 of 18 tools now native in TS.

stage3

Port the data-coupled tools

search, platform-vfs, document-question, matter-info, matter-messages — calling matter / content / search through @lawhive/content-framework, the way lawrence-api does. Preserve the server-side person-id guard (identity from request context, never LLM args) and platform-vfs's client-fulfilled PatchPayload contract. No bridge.

stage4

Delete the Python agent-gateway

With every tool in TS, the Python agent-gateway service has no job left — the loop is the chat api. Decommission it; the only Python that remains is lawrence-engine and ingestion.

Top risks

RiskSeverityMitigation
Residency — state / code-exec inputs / prompts in US infraCriticalSelf-hosted Inngest (eu-west-2); no Vercel Workflow; AgentCore eu-west-2 for code execution; in-region model proxy if gateway residency unconfirmed.
ai-stream wire-format parityHighGolden-fixture conformance test: capture real Python emitter output, assert TS output is shape-identical. This seam is the consolidation payoff and the likeliest silent regression.
Citation subsystem (no native AI SDK analogue)HighOwn CitationConfig in the TS loop (stage 0); emit citations as source/data parts. Getting it wrong silently drops citations in legal answers.
Cross-zone signing (ingestion search-service)MediumThe one residual KMS hop. Port that signer and fixture-validate against real signed requests; in-tier service calls need no signing.
person-id injection guardHigh (security)lawrence-api rejects LLM-supplied person/identity IDs today. The TS tools must keep that: identity comes from the authenticated request context, never tool input params. Assert in contract tests.
legal-sources AKN/Atom XML parityMediumFull TS port with a fixture corpus; isolated Python parser microservice as a bounded fallback (no crypto/PII in this path).

Frameworks we benchmarked

Before committing to a thin in-house framework on the AI SDK, we wrote up the two strongest off-the-shelf harnesses against this exact proposal. Neither changes the recommendation — one is worth adopting later, the other worth borrowing from.

Mastra's Agent Harness (full evaluation) — built on the same Vercel AI SDK we're porting to, so its harness (sessions, observational memory, approvals, sub-agents, modes) drops into lawrence-api as a library: our own Postgres via pg.Pool, an Inngest-backed runner for durable runs, no separate runtime and no leaving our base. It's the one framework worth adopting for the deferred pass-2 layer if we choose not to build those primitives ourselves. Verdict: viable pass-2 adopt — revisit when we reach sessions and memory.

Flue (full evaluation) — the Astro team's headless, durable-first harness (Apache-2.0) is the mirror image: a clean design, but built on Pi / pi-ai rather than the AI SDK. Adopting it means re-platforming off our base, embedding into Hono instead of Express, and betting legal PII on a one-week-old beta — and its remote() sandboxes reintroduce the US-pinning we rejected for Vercel Sandbox. State, at least, is self-hostable on our own Postgres in eu-west-2. Verdict: don't adopt — but borrow its Durable Streams design (append-only event log, resume/reconnect, pluggable Postgres store) as the blueprint for our deferred durable-sessions work.

What v7 does to this (June 2026). AI SDK v7 now ships the headline pass-2 primitives natively — durable/resumable runs (WorkflowAgent, self-hostable via @workflow/world-postgres) and tool approvals (HITL) — on our own base. That partly supersedes both verdicts: the case for adopting Mastra in pass-2 shrinks (the SDK now gives us sessions / approvals / durable runs directly), and Flue's "borrow Durable Streams" is largely redundant (the SDK has it). Both stay useful as references, not dependencies.

What stays Python — forever

lawrence-engine (CCO, material-case-change, proactive) and ingestion are out of scope and stay Python — they aren't agent loops, and they're genuinely separate services. That's it. There is no Python data layer to keep on the agent side: matter data lives in lawrence-api (TS) and the domain services, and content-retrieval — the Python client lib that reached them — is deleted, because the TS tools call those services directly via the shared framework. The tools decrypt nothing, so nothing crypto-sensitive crosses a language boundary — because nothing crosses at all. Inngest stays polyglot; the TS loop enqueues its Python functions via the TS SDK.

Open questions

One line: cap-02 already rewrites the loop and lawrence-api is already TS, so port the whole loop to TypeScript and place it in the api tier — the loop in the chat api (B-fit-1, recommended) or a sibling agent-runtime api. content-retrieval and kms-utils are transport, not a data layer, so every tool ports (calling services via @lawhive/content-framework) and the Python agent-gateway is deleted; keep self-hosted Inngest; and hold Vercel Sandbox/Workflow out because both default to US-pinned iad1 and the data is UK/EU legal PII. And prove the platform first on a small matter-opening agent, off Lawrence's path, before porting the loop.