previews · Architecture & design · Agent Platform M3

Agent Platform · M3 requirements · July 2026

Agent Platform M3: full Lawrence 2 on TS

The migration payoff. M3 ports the production Python Lawrence 2, the VFS chat agent, onto the TypeScript framework in lawrence-api, so /lawrence/chat is served by an in-process defineAgent loop instead of a cross-repo call into Python. This is a reader-facing recut of the confirmed requirements, published so you can react to the scope decisions before units and PRs are cut.

Scope confirmed · pre-planning Requirements explainer The migration payoff LEX-644 LEX-645 Linear project →

platform-v3/docs/plans/2026-07-03-003-feat-agent-platform-m3-full-lawrence2-on-ts-plan.md

TL;DR

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.

Todaytwo repos, two languages, a round trip
KMS-signed HTTP · cross-repo /vfs every read + write Inngest event v3 sidebar lawrence-api /lawrence/chat Python ChatAgent agents repo ingestion · OpenLaws Exa · CourtListener lawrence-engine Python · form filling
After M3one repo, one process, adapters direct
direct call was /vfs HTTP same Inngest same v3 sidebar lawrence-api defineAgent loop · in-process adapters + VFS, no HTTP hop domain services external APIs unchanged lawrence-engine Python, stays

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.

read41
create16
edit7
statalways-on, non-sidebar0

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.

ask_question_about_documents78
todo_write69
search28
get_matter_info25
todo_read18
vector_search14
get_matter_messages14
web_search9
search_legislation8
load_skill7
read_legislationpaired citation reader0
search_case_lawCourtListener / Find Case Law0
read_case_lawpaired citation reader0
check_case_statusUS-market gate only0
fill_formforms flag · fires Inngest0
ask_user_questionclient-fulfilled, added upstreamnew

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/system prompt

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.

Above the waterline
The tools (LEX-644). Roughly twenty tools: the half everyone pictures when they hear "migrate the agent".
parity waterline
Stream protocol
UI-message-stream v1 plus nine custom parts (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.
Prompt composition
Langfuse prompt selection by surface, with label precedence and TTL caching, plus about 18 compile variables regenerated from the TS toolset (toolsXml, skillsCatalogXml, citationStandardsXml, namespacesConfig and more). Prompts are re-versioned for the verb-tool shape.
Context assembly
Matter retrieval with fallback (parties, files, case-context), team and client context, jurisdiction and market, recent documents, custom instructions, and localisation directives.
Message pipeline
Validation, invalid-tool-part removal, stale filtering past 12 hours, compaction splicing, and model-format conversion with full tool-output reconstruction.
Compaction
Keep the last 10 turns, a 0.75 ratio threshold, 16,384 reserved output tokens, a structured summariser, durable data-compaction replay on later turns, and data-compaction-progress events. Without those events the frontend shimmer never clears.
Citation processing
Streaming marker parse, reference validation and dedupe, ref-N assignment, data-reference emission, then inline rewrite. All gated by the embed-citations flag.
Guardrails
Server-resolved identity (person, firm, actor) so no model-supplied principal is ever trusted, matter-id override with security events, nonce-wrapped custom instructions, path validation, source-id stripping, and tool-error redaction.
Model behaviour and cancellation
Langfuse-config-driven model selection, Anthropic cache-breakpoint placement, file-block hoisting, fine-grained tool streaming, cache-token usage feeding the analytics columns, and the Redis per-thread cancel key honoured every loop iteration.

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.

Tier 1 · always present
Skills catalog in the prompt

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.

load_skill(name)
Tier 2 · on demand
Full body + reference files

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:

Platform-mechanics agent-discoverable repo files
drafting file-organisation form-editing legal-research
Business-aligned user-triggered Skill records
advice-note briefing-note case-summary email-drafting

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.

Flag off
Python path
The cross-repo ChatAgent, unchanged. Where flag-off users and any fallen-back thread run.
per-thread kill-switch
Flag on
TS in-process loop
/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

One action before planning

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: