Lawrence engineering · Strategic plan · June 2026

One loop, many Lawrences

Today the agents backend is a single 1,500-line chat agent welded to an HTTP request. Six capabilities, built in dependency order, turn it into a runtime that hosts many agents, survives a reload, runs in the background, and reacts to matter changes on its own. This is the sequence, and what each step unlocks.

agents repo sequence, not a calendar 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).

Pick a Lawrence. See what it costs.

Every feature we want next lands on the same handful of capabilities. Select one of the things we want to build and the ladder lights up exactly the steps it needs, in the order it needs them. The dashed amber steps are platform-side prerequisites that live in other repos.

01
Generalised Agent API
Route by slug. Optional matterId. Generic agentParams + principal.
routing layer
02
Framework refactor
BaseAgent + hooks: prompt, context, tools, vfs. Emitter seam.
keystone
03
Durable streams
Redis stream + chunk-id cursor. Reload & hook back. Server-side persistence.
substrate
04
Background execution
Run as an Inngest function. Event creates the thread. Agent acts as the user.
substrate
05
Triggers
React to MCC / CCO / cron events. Proactively act on the matter.
capability
06
Code execution
Sandbox + VFS bridge + policy gates. The harder agent abilities.
capability
After 01 + 02: many HTTP Lawrences, cheap

What do you want to build?

Select a scenario above to trace it through the ladder.

Why this shape

The Next Steps doc is right that the next set of upgrades needs a more holistic lift of the agents infrastructure, not another feature bolted to the side. The evidence is that every initiative on the roadmap converges on the same place in the code. Today ChatAgent.generate_stream() is one method, around 780 lines inside a 1,500-line class, that interleaves prompt selection, context loading, the tool loop, citation parsing, tracing, and SSE rendering. A new surface means another elif in prompt routing; a new mode means another boolean; a new agent means copying the whole thing.

Boolean modes don't scale. The cure is the same five seams whichever feature asks for it first.

So the plan is not eight tracks competing for attention. It is a capability ladder: a generalised API and a framework refactor first, which between them let us stand up many agents over HTTP cheaply, then durability, then background runs, then triggers, with code execution last and demand-driven. Each rung is useful on its own, and each one is a precondition for the rungs above it. The order is the point.

The sequence

Roughly this order, not strictly. Each capability below carries the design notes that actually decide whether it lands cleanly.

01Generalised Agent API

POST /agents/{slug}/chat routed by slug, slugs registered in code (agents as code). One envelope { threadId?, messages, agentParams } where agentParams is generic and the agent validates it against its own Pydantic schema. matterId stops being mandatory and moves into agentParams for the agents that need it.

02Agent Framework / ChatAgent refactor

BaseAgent owns the base logic (loop, tool calling, streaming) with method hooks for the customisable parts and mixins for the optional ones. Each agent becomes a thin definition. Done by parts as needed, behind the existing route, with a stream-protocol fixture as the tripwire.

Milestone — after 01 + 02

With routing and a hookable loop in place we can add HTTP-only agents, but many of them, cheaply: Global Lawrence, the library precedent drafter, a skill-drafting assistant, Lawra for admin, a client-app Lawrence, even title-gen and eval agents are now just slugs with a context hook and a tool set. Everything below adds reach in time (durability, background, reaction); the breadth of who Lawrence can be is unlocked here.

03Durable streams

Fire an agent, reload the tab, re-attach to the running stream. A Redis stream per run, every chunk gets an id, the client reconnects with a last-chunk-id cursor. Pattern A, the same shape as the durable-llm-streams POC. It splits into two halves, and we already have the first.

04Background execution

Run the agent as an Inngest function with an event consumer in lawrence-api that creates the thread. Output reuses AIThread/AIMessage with a kind field (user_chat | scheduled | proactive). No new entity.

05Triggers

An agent subscribes to Inngest events (material case change, CCO production, user cron). Target state: Lawrence reacts to a matter update and proactively does something for the lawyer, reporting back in a new thread.

06Code execution / sandbox

The harder agent abilities. No longer hypothetical — two concrete drivers already exist: the numerical amount calculations the current form-filling pipeline does in code (which agentic form filling has to keep), and court bundle assembly (merge / paginate / index matter PDFs). Still demand-sequenced and last in the order, but the demand is here.

The demo we faked, made real

On one demo matter, a thread looked like Lawrence had noticed an intake file land, read it, and drafted an email for the lawyer to review, unprompted. That is capabilities 02, 03, 04 and 05 working together. The one wrinkle — the run writes without being asked — needs no policy layer: a VFS email is always a draft, never sent, so the review step is built into the write surface. Here is the real trace.

proactive run · agent_slug: proactive · kind: proactiveno connected client
1
event app/case/file.uploaded → trigger (05) matches the matter-intake rule, enqueues a background run (04).
2
auth Inngest function starts as the matter's lawyer via the stored permission grant. No session, no browser.
3
read vfs:// read the intake file and the current CCO through the agent's vfs_config().
4
create Lawrence calls create on emails:// — no send tool exists, so the write is a draft by construction. Create is server-fulfilled, so the draft is written server-side and this works in a background run today.
5
persist tool outputs + draft written server-side (03), since no client exists to forward them.
6
notify consumer opens a new kind: proactive thread; the lawyer sees "I drafted this for your review" on next visit.
The same path serves autonomous action on a material case change: swap the trigger for should_fire_material_change_event, feed the CCO diff since the last MCC as context, and give the agent the safe live-write tools (create task, set key date, add note). It still has no send tool, so anything client-facing stays a draft for review — auto-execute is just which tools the agent holds.

The other backend: lawrence-api

Half of this plan lives in a repo the ladder doesn't draw. lawrence-api (platform-v3, TS) owns the thread model, the persistence, the auth boundary, and the client-fulfilled write paths — so it's in scope as a strategic backend in its own right, not a list of favours from another team. The agents-side envelope generalisation (01) has a direct counterpart here, and it's the biggest piece: the thread model.

Generalising the thread model beyond matters

Today AIThread is matter-anchored by construction: matterId is a required column, the uniqueness and index keys are [matterId, userId, singleton], every create and list path requires it, and access is authorised by authRouteForMatter("FIRM") against matter-service. There is no firm, user, or org scope — the lone exception is AICustomInstructions, already user-scoped, a useful precedent that non-matter AI state can exist. Global Lawrence, the library precedent drafter, Lawra (admin) and a client-app Lawrence are none of them matter-scoped, so today they have nowhere to persist.

The clean move mirrors the envelope: just as 01 makes matterId optional and folds it into agentParams, the thread carries a generalised scope instead of a hardcoded matter.

Thread scope is not the same as data-access scope. A Global Lawrence thread is user-scoped, yet reads across many matters.

That distinction is the trap. A global thread belongs to the lawyer (user scope), but its tools read many matters' metadata — a data-layer concern (permission-scoped cross-matter reads), not a thread-scope one. Generalising where a thread lives and generalising what an agent can read are two separate problems; conflating them is how the isolation guarantee gets lost. This sub-section is the persistence half of Threads M2 / Global Lawrence, already on the roadmap — here aligned with the agents envelope so both halves move together. (AIMatterMemory hits the identical question the moment memory goes firm- or user-wide.)

The rest of the lawrence-api surface

What depends on what

The order is a dependency graph, not a staffing plan. What must come first, and what can move at the same time:

START · IN PARALLEL AFTER 02'S SEAMS THEN LAST LAWRENCE-API · OWN LANE, CAN START NOW 02 · Framework refactor KEYSTONE 01 · Generalised API 06 · Code execution min + full sandbox 03 · Durable streams full · agent owns stream ✦ Many Lawrences (01+02) 04 · Background + agent-as-user auth 05 · Triggers 03 · Durable streams client half · in flight Thread model scope, not matter Server-fulfilled edits email/note · #8C
keystone milestone depends on cross-repo (lawrence-api) lawrence-api lane — can start now
Left–right is rough sequence, not a calendar; nodes in the same column run in parallel.

So from day one, three things can move at once: 01 + 02 on the agents side, the client-facing durable-stream half on the lawrence-api side, and the thread-model generalisation. The 04→05 chain and 06 open up the moment 02's seams land.

Not doing this horizon

Full framework migration (LangGraph/Mastra) — the call is stay-home, decompose first. Voice mode waits on durable streams. Memory/personalisation beyond keeping the nonce-composer seam. An agent config UI — agents-as-code per Next Steps. (Code execution is no longer on this list — it has two real drivers now, see 06 — but it stays last in the order, gated on the VFS bridge.) The point of writing these down is so the ladder stays a ladder.

One scope boundary worth stating: the VFS-native, cited, conversational, proactive message/email drafting experience is its own product project. This plan only covers retiring today's bespoke message-drafting service directly, the moment structured output exists — parity, minus a deployable unit. The nicer experience is built on top, separately.

Copied