Today a chat turn crosses a repo and a language: lawrence-api signs an HTTP call and hands the
turn to a Python agent, which then calls back into /vfs for every read and write.
M3 collapses that seam. The loop runs in-process in lawrence-api, calling the
content adapters directly.
Four VFS tools subsume into verb-tools, sixteen standalone tools port with their production gates, the two-tier skills runtime is rebuilt in TS, and the streaming, prompt, context, compaction and citation machinery reaches behavioural parity before a flag-gated cutover with a per-thread fallback to Python.
The behaviour contract is today's agent, exactly as it runs: improvements, including the fix for the LEX-533 client-fulfilled silent failure, are separate decisions, not silent side effects of the port. All six scope questions are resolved. One planning-time action stands: an inventory refresh against upstream.
01The seam moves in-process
Start with the picture, because the whole milestone is one structural change. Today the v3 sidebar hits
lawrence-api's /lawrence/chat, which signs an HTTP call with KMS and hands the turn to
the Python ChatAgent in the agents repo. That agent then calls back into
lawrence-api's /vfs for every domain read and write, reaches out to ingestion,
OpenLaws, Exa and CourtListener, and fires an Inngest event to lawrence-engine for form filling.
After M3, lawrence-api runs the loop itself. It calls the content adapters directly, so the
/vfs round trip disappears; it makes the same external calls; and it fires the same Inngest event.
lawrence-engine stays Python, and form filling does not migrate.
The win is not a new capability. It is one repo and one language. Today every namespace, tool or behaviour change touches two repos in two languages, and only a handful of engineers work the Python side: the exact bottleneck the Agent Platform project exists to remove. After M3 that change is a single TypeScript change.
02The tool census
The visible half of the port is the toolset. Here is every tool in the production chat agent,
sorted by what happens to it. Counts are 30-day Langfuse for the flagged chat-response agent, about
405 tool calls across roughly 330 traces. They are directional, not drop-safe: no live tool was dropped on usage
alone, and every drop was adversarially verified.
Subsume into verb-tools
The four VFS tools become read / create / edit /
stat verb-tools with a namespace argument. The framework has read and
list today; M3 finishes LEX-640's write verbs.
create is server-fulfilled; edit covers six surfaces (emails stay
client-fulfilled, notes goes server-fulfilled); stat is kept for artifact field schemas.
Port as standalone tools ×16
Each ports with its production gate intact. Zero-usage tools that are still registered and gated stay: thin counts reflect flag volume, not disuse.
Dropped: adversarially verified dead
Seven identifiers do not port. None on usage: three are retrieval-agent-only and never registered in the chat agent; four are modules deleted from source.
- reformulate_queryretrieval-agent only
- decompose_queryretrieval-agent only
- retrieve_documentsretrieval-agent only
- faq_toolmodule deleted
- legal_search_toolmodule deleted
- edit_toolmodule deleted (not the live VFS edit verb)
- read_toolmodule deleted
Out of scope with the legacy chat mode
The VFS path is the only migration target. The legacy chat mode retires with the Python service.
- classify_intentintent / planning routing
- search_legal_practice_guideslegacy
agent/systemprompt
Flag-off users stay on the Python agent until the Lawrence 2 default rollout completes. Whether a practice-guides search belongs in the VFS toolset is a product follow-up, not a parity requirement.
03Parity is mostly not the tools
If the port were only the tools, this would be a small milestone. It is not. Below the toolset sits the runtime that makes the agent behave like the agent. Frontend rendering, the compaction shimmer, citation links and the resume-into-the-same-message flow all key off exact wire shapes: a rename or a missing part type breaks the UI silently. Every layer below has to reach parity before the flag can turn on.
data-thinking,
data-trace, data-thread-model, data-message-usage,
data-reference, data-tool-execution-delta,
data-tool-invocations-complete, data-compaction,
data-compaction-progress), a 15s heartbeat and the [DONE] sentinel. Golden
fixtures validate it against the frontend part schema.toolsXml, skillsCatalogXml,
citationStandardsXml, namespacesConfig and more). Prompts are re-versioned for the
verb-tool shape.data-compaction replay on later turns, and
data-compaction-progress events. Without those events the frontend shimmer never clears.ref-N assignment,
data-reference emission, then inline rewrite. All gated by the embed-citations flag.04The decisions
Condensed from the confirmed Key Decisions, each with the alternative named. These are the calls worth reacting to now.
VFS path only
The migration target is the Lawrence 2 path the sidebar uses. The agent/system prompt,
classify_intent and search_legal_practice_guides retire with the Python service.
Not: porting both chat modes. Flag-off users stay on Python until the L2 default rollout.
Production wire names win
The frontend keys rendering, cards and refresh off exact tool-<name> part types. Ported
tools keep their production names; the framework's vfs_read / vfs_list naming yields.
Not: keeping the framework names behind a rename shim, which silently breaks the UI.
Client fulfilment shrinks to emails
Notes edits go server-fulfilled, riding the in-flight notes migration to the content service. Email edits port client-fulfilled as-is, including the LEX-533 silent-failure behaviour.
Not: fixing email server-fulfilment inside M3. It is unplanned and FOX-owned, so deferred.
Skills split by what they teach
Platform-mechanics skills, those teaching the model how to drive the platform, the VFS and its tools, ship
as CI-validated repo files in lawrence-api, discovered through the catalog and
load_skill. Business-aligned work-product skills become records on the v3 Skill
model, delivered by the UI's existing skill-trigger (it pastes the body into the conversation as a prompt),
not discovered by the agent.
Not: porting all eight as agent-discoverable repo files. That keeps parity but blocks
firms from owning the work-product skills, and leaves the Skill table an unused parallel product.
Build on the merged API
Tool and skill definitions are written against what is on develop today.
Not: blocking M3 on the post-feedback redesign (fluent builder, .with()
plugins, sealed registry): that lands as an independent refactor.
Observability at an ops floor
M3 carries session and user attribution and cost accounting, enough to operate and debug the cutover.
Not: full Python-parity span shaping, trace naming and tags: that stays in LEX-642, startable any time.
05Skills: two-tier disclosure, split by what they teach
LEX-645 rebuilds the static-skills runtime in TS, and revises where skills live. The runtime stays a two-tier design so the prompt stays cheap: a catalog of names and descriptions lives in the prompt, and the full body loads only on demand. What is new is that only some skills go through it. Skills now split by what they teach, and only the platform-mechanics ones stay agent-discoverable.
A flag-gated skillsCatalogXml slot lists each repo skill's name and description. Cheap, and
enough for the model to decide what it needs. Only the platform-mechanics repo skills appear here.
The tool fetches the skill body, then its reference files when asked, with path containment and an unknown-skill error that lists what is available. It resolves repo skills only.
The split turns on what a skill teaches, and it decides the delivery path.
Skills that teach the model how to drive the platform, the VFS and its tools stay as repo files, discovered through
the catalog above. Skills that encode a work product a firm would want to own and customise become records on the
v3 Skill model, riding the skill-trigger the UI already has today rather than being discovered by the
agent. The classification below is provisional, confirmed against each skill's body at planning:
The four platform-mechanics skills port as repo files with their reference
corpus, and stay in the catalog and load_skill exactly as today. The four business-aligned skills are
recreated as Skill model records with equivalent content, but they are not agent-discoverable: they
never appear in the catalog XML and load_skill does not resolve them. They reach the agent through the
UI's existing skill-trigger, which pastes the skill body into the conversation as a prompt. That path is already
built and live, and M3 does not touch it. This is a deliberate parity divergence: today all eight are
catalog-discoverable in the Python agent, and after the migration the agent discovers only the platform-mechanics
four. Bodies are rewritten wherever tool names or VFS paths changed, not copied, and the stale reference in
legal-research (it names a tool that no longer exists) is fixed. Per-skill gating survives, so
form-editing disappears when the forms flag is off, and activation tags the trace
skill:<name>. Skill-to-skill references by name keep working (drafting loads
legal-research, both repo-side under this split). CI validates every repo file; the Skill
records are validated at the model boundary. Skills land after the tool port, so each body is
written once against final tool names.
06Rollout: flag-gated with a live fallback
The cutover is reversible per thread, and the flag does not turn on until conformance is proven. The Python service stays deployable through burn-in.
ChatAgent, unchanged. Where flag-off users and any fallen-back
thread run./lawrence/chat served by the framework loop, with existing thread persistence
and analytics wired into the finish hook.Golden-fixture gate
Stream-conformance and compaction round-trip tests pass before the flag turns on. The eval harness keeps its snapshot no-op writes, so evals never mutate real data.
Mixed-runtime continuity
Threads with Python-authored messages replay through the TS pipeline, and TS-authored threads replay through Python for the rollback window.
Cost and latency budget
Time-to-first-token and per-turn cost are measured before and after, against an agreed regression budget.
No new exposure
The loop moves from the Python service's EU region into lawrence-api's US-East prod, where
v3 already processes matter content. Noted in the cutover record, not gated on sign-off.
07Open items and the inventory refresh
All code evidence was read at an agents checkout seven commits behind
origin/develop. Upstream has already added ask_user_question and the client-fulfilled
tool primitive. Planning starts by re-verifying the tool inventory and message-pipeline behaviour against
current upstream, so the census above is treated as a strong draft, not a frozen manifest.
The scope questions are resolved. What remains are planning-time decisions, each an open question with a known shape rather than an unknown:
- search vs vfs_list. How the cross-namespace
searchreconciles with the list verb, rather than silently merging them. - Tokenizer parity. Match the Python compaction threshold, or re-tune the ratio for a TS tokenizer.
- Shared-skills home. The central directory for shared skills and the shape of the include API.
- Business skills as records. Confirming the per-skill classification against each body, references handling for
case-summaryas a single-body prompt, and seeding and ownership of the business-skill rows. - Flag evaluation. Where it lives (middleware vs the loop) and whether the userId contract tightens.
- Eval acceptance bar. The harness choice and the numeric cutover bar, plus porting or bridging the Python suite.
- Sidebar read-only flag. Whether it is retired or its semantics inverted so the new sidebar gets the full-capability prompt.