previews · Architecture · The loop, in TypeScript · Topology

Lawrence engineering · Companion to "The loop, in TypeScript"

Runtime topology, against the v3 tiers

v3 layers apps → apis → services: apis are the orchestration/gateway tier that calls many services to enrich a response; services are domain-isolated and signal change over events. An agent loop calling tools is api-tier work — so the runtime is an api, not a new layer above the apis. The cycle today is the Python loop, deployed as a service, calling back up into lawrence-api's /vfs. Here's the current path and the two convention-fitting fixes.

app api / TS Python (today) service / mesh call (down a tier) the cycle (calls up) event (async)

Today current · the cycle

Verified from apis/lawrence-api and the agents repo. lawrence-api (api) invokes the Python agent-gateway (a service); the agent's tools then call back up into lawrence-api's /vfs. That up-arrow — a service reaching into an api — is both the cycle and the layering break. It exists because the Python loop can't reuse lawrence-api's TS VFS adapters, so it reaches back over HTTP.

APPS APIS SERVICES legal-os useChat · Clerk JWT lawrence-api · api (TS) invoker ChatAgentClient tool provider /vfs router owns AIThread · matter-ACL authority agent-gateway Python service · the loop tools hold no data — every read/write calls back to /vfs the anomaly: a service, calling up domain services matter · content · messaging email · assignment · work (content-framework adapters) POST /lawrence/chat invoke api→service tools → /vfs service ↑ api /vfs → services ↻ cycle + layering break
The cycle is a layering break, and the language split is its cause. A service (agent-gateway) calls up into an api (lawrence-api /vfs) because the Python loop can't import lawrence-api's TS VFS adapters. Rewriting the loop in TS removes the cause: a TS runtime imports the shared @lawhive/content-framework and calls services down, like lawrence-api does.

B-fit-1 — put the loop in the chat api recommended

The loop is api-tier orchestration, and lawrence-api already owns chat + VFS + the AIThread model — so the loop belongs there. Tools become in-process calls through @lawhive/content-framework + its service clients. The Python agent-gateway is deleted; every edge points down the tiers.

APPS APIS SERVICES legal-os useChat lawrence-api · the chat api — now runs the loop agent loop AI SDK · streamText owns AIThread + stream stopWhen · citation parser VFS tools @lawhive/content-framework + service clients called as functions in-proc domain services · matter · content · messaging · email · … /lawrence/chat tRPC service clients
One api, the standard app→api→service flow. No new tier, no api→api, no cycle — and the only option that fully sheds the /vfs hop, the KMS signing, and the content-retrieval client layer, because tools call the shared framework as functions. The long-lived chat workload is isolated by deploying the api as its own pool (a deploy choice, not a layering one). Cost: the runtime lives in platform-v3 and shares the api's lifecycle.

B-fit-2 — a sibling agent-runtime api if you want independence

For independent deploy/ownership, the runtime is its own api in apis/, beside lawrence-api. It owns the AI thread model and builds its tools from the same @lawhive/content-framework + its own service clients — calling services directly, never lawrence-api. Triggers arrive as Inngest events, the documented way services signal change.

APPS APIS SERVICES legal-os useChat agent-runtime · api the loop + AI thread model owns durable stream VFS via content-framework the agents repo, in TS lawrence-api · api sibling · non-agent surfaces + platform-api, lead-api same tier — not called by the runtime ✕ no api→api domain services matter · content · messaging · email identity · assignment · work shared @lawhive/content-framework adapters Inngest events → triggers durable / background cap-04 / cap-05 useChat other surfaces content-framework + clients tRPC emits wakes the runtime (async)
Two sibling apis, each orchestrating services down. The runtime never calls lawrence-api, so no api→api edge and no cycle; the AI thread model moves to the runtime so it doesn't reach up for state. Keeps the runtime independently deployable and LEX-owned, at the cost of a thin internal hop to services and running two chat-adjacent apis. Proactive triggers (cap-05) arrive as Inngest events — the documented service pattern — never a sync call up.

Rejected — runtime as an api above lawrence-api

Stacking the runtime on top so it calls down into lawrence-api (app → agent-runtime → lawrence-api → services) is gateway-on-gateway: an api→api edge in the one direction v3 keeps rare, and it puts the tool surface back behind another api — which is exactly the call-up that reintroduces the cycle. The fix in both shapes above is the same: tools reach the services tier through the shared @lawhive/content-framework, never back up into an api.

The rules that keep it acyclic

01

Calls only go down the tiers. app → api (orchestration) → service (domain). Never up.

02

The agent's tools reach the service tier via the shared content-framework — not back up into an api's /vfs. That up-call is today's whole cycle.

03

The chat api owns its state. AIThread, the durable stream, and auth live with whichever api runs the loop, so the runtime never reaches up for them.

04

Cross-domain triggers go over Inngest events, the documented service pattern — a service emits, the runtime wakes. No upward dependency, even for cap-05.