Mastra just shipped an Agent Harness: sessions, memory, steer/interrupt,
tool approvals, subagents and modes wrapped around the agent loop. It's the layer we deliberately hand-rolled
thin and deferred. The one fact that decides everything: it runs on the same base as us — the
Vercel AI SDK — so it isn't a competitor to our bet, it's an option for the next layer. And it
embeds in lawrence-api, backs its state on our Postgres, and goes durable on the
Inngest we already run.
By Adolfo Tamayo Briceno · Lawyer Experience (LEX)
same base: AI SDKlibrary, not a deploymentstate on our DB · durable on Inngest
Our framework
thin defineAgent, VFS, two transports · first pass shipped
Mastra Harness
sessions, memory, approvals, subagents, modes
↓ both sit on ↓
Vercel AI SDK v6
model providers · tool calling · the loop
Update · AI SDK v7 (GA 2026-06-25). v7 now ships the pass-2 layer this doc weighed
adopting Mastra for — durable/resumable runs (WorkflowAgent, self-hostable via
@workflow/world-postgres in eu-west-2), tool approvals (HITL), and typed tool
context — on our own base. So the "viable pass-2 adopt" verdict weakens: the SDK now gives us most of
the harness directly. Mastra stays a reference for ergonomics, not a likely dependency. See
the migration doc for the v7 base.
The one fact that frames the rest
Mastra is built on the Vercel AI SDK. Since its v1, Mastra runs the agent loop and tool
calling on top of the SDK's LanguageModelV2 providers — the exact base our first pass uses.
So the harness is not an alternative to the decision we already made (TypeScript on the AI SDK,
self-hosted). It's a higher layer over that same base — the layer we built thin and left most
of for later.
A YC-backed framework at v1, on 300k weekly downloads, chose the same base we did. That's
confirmation of the foundation, not a reason to rethink it.
Which turns “Mastra vs us” into a far more useful question: for the capabilities we
deferred — durable runs, sessions, memory, human-in-the-loop, subagents, modes — do we
hand-roll them on our thin framework, or adopt Mastra's harness for that layer? The rest of this page works
that out, including the two things that usually kill adoption: where the state lives and
whether it needs its own runtime.
What the harness adds over a bare loop
Everything here is what Mastra wraps around the loop the AI SDK already runs. Read it as a
menu of the layer above streamText.
Capability
What it does
Sessions
Persistent multi-turn threads, keyed by threadId, written to a storage backend; create / switch / rename / clone.
Observational memory
Compresses long runs via observer → reflector agents (observations merged) instead of lossy summarisation.
Steer & interrupt
steer(), follow-up queuing — a conversation you can watch, interrupt and redirect mid-run.
Tool approvals
Per-tool / per-category allow/deny; a built-in ask_user that suspends the run; respondToToolApproval().
Subagents
Delegation to subagents exposed as a tool — isolated (fresh context) or forked (cloned parent conversation, warm cache).
Modes
Specialised agents you switch between, with transitionsTo handoff after approval.
Event stream
~35 signals collapsing into a HarnessDisplayState (currentMessage, activeTools, pendingApproval, activeSubagents, tasks) for the UI.
Side by side with our first pass
Same base, different altitude. Our column is what shipped in the
first-pass stack; the deferred rows are exactly what Mastra
already ships.
Our framework (pass 1)
Mastra Harness
Base layer
Vercel AI SDK v6, direct
Vercel AI SDK, under the hood — same
Author surface
defineAgent({...}), thin & ours
Agent + Harness classes
Lives in
lawrence-api
lawrence-api (library) — same
Transports
run + streaming (UI message stream)
event stream + displayState; resumable via DurableAgent
Sessions / persistence
deferred stateless in pass 1
built-in our Postgres via PostgresStore
Memory / compaction
deferred
built-in observational memory
Durable long runs
deferred (the durable-streams PoC)
nativeDurableAgent + Inngest runner
Subagents
roadmap (“many Lawrences”)
built-in isolated / forked
Modes / handoff
not built (≈ our surface idea)
built-intransitionsTo
Human-in-the-loop
none control via VFS exposure, writes are drafts
built-inask_user suspend + approvals
Data access
our VFS over @lawhive/content-framework, our services, auth, Langfuse
Mastra tools — our VFS would be custom tools
Yes — it runs in lawrence-api, on our DB
The two questions that usually decide build-vs-adopt. Both answers landed on the easy side.
Does it need its own runtime?
No. Agent, Harness and DurableAgent are classes in
@mastra/core. You new them inside our existing Express handlers — Mastra
explicitly supports embedding in an existing Express/Fastify/Next app, and ships
createServe(expressAdapter)({ mastra, inngest }) for our own routes. Mastra's server, playground
and Cloud are optional and we skip them. The harness sits behind our usual
authRoute → requireAgentsFlag → authRouteForMatter chain like any other route.
Do we have to use their state model?
No separate store. PostgresStore from @mastra/pg accepts an existing
pg.Pool — “if you already have a pg.Pool in your application, pass it directly.”
lawrence-api already has one. Mastra owns its tables (threads, messages, memory, run snapshots) but they live
in our Postgres, in eu-west-2, on our pool. We let Mastra own its session store rather
than merge it with AIThread; a fully custom adapter is possible if we ever need our schema to be
the source of truth.
Durable long runs, in one service
The headline harness use case — runs you watch, interrupt and that outlive a request —
is exactly the durability we deferred. Mastra wires it onto Inngest natively, and the three substrates it needs
are already in lawrence-api: Inngest Connect, a Postgres pool, and Redis.
Everything runs in lawrence-api: the harness as a library, its workflow steps as
Inngest functions over the Connect we already run, run snapshots and memory on our
Postgres pool, resumable streams over Redis. Compute and state are in eu-west-2; Inngest Cloud
holds only orchestration metadata (US) — unchanged from today's posture.
How a durable turn actually flows
A request hits /agents/:slug/chat, our auth/flag/matter chain runs, then the handler drives a
DurableAgent (or a Mastra workflow). createStep calls become Inngest steps, so
completed steps are memoised — a retry, a deploy, or a resume skips them. When the agent hits an
ask_user or a tool that needs approval, the run suspends: its state snapshot is
written to our Postgres and an Inngest wait holds it — for seconds or for days — until a
lawyer responds, at which point resume(runId, resumeData) picks up exactly where it left off. The
client can drop and reconnect with observe(runId), replaying missed events from the PubSub cache.
None of that is ours to build.
The durable-streams PoC, human-in-the-loop review, and long-run memory — three things on
our deferred list — are one library and the Inngest we already operate.
Where it doesn't fit — the honest costs
Adoption isn't free. None of these are infra (it's a library), but they're real.
Conformance. Our agents would take Mastra's Agent/Harness/workflow shape rather than our thin defineAgent, and our VFS becomes Mastra tools. We trade our surface for theirs.
A parallel state model. Mastra owns its threads/messages/memory tables — separate from AIThread. That's duplication to reconcile, and matter PII in those tables is a residency surface to vet (in-region, but new).
Dependency weight + cadence.@mastra/core is a large, fast-moving v1 dependency for a load-bearing layer. We'd be on Mastra's release treadmill.
AI SDK v6 compatibility. Mastra v1 runs on AI SDK v5/v6 providers; we're on ai@6 with our own provider registry. Likely fine, must be confirmed before betting.
Inngest Cloud is still US for orchestration metadata. Unchanged from today, and the same levers apply (coarse steps; PII in our DB, not Inngest) — but adopting the durable path doesn't improve it.
Recommendation
Nothing here changes the first pass. It sharpens the build-vs-adopt call for the layer above it.
Keep pass 1
Ship our thin defineAgent framework as-is — it's integrated with our VFS, services, auth, Langfuse and Inngest.
Don't rip it out for Mastra; the two agents prove the spine.
Borrow now
Lift the harness patterns as we hit them: ask_user suspension, subagent-as-tool, mode handoff, the displayState event shape.
Revisit our “no approval layer” stance for matter-intake-style review.
Adopt? pass 2
For durable runs, sessions, memory, HITL, subagents — seriously evaluate adopting the harness rather than hand-rolling.
It embeds in-service, on our DB, on our Inngest. Gate on a spike.
The spike: in lawhive-mastra-poc, stand up createDurableAgent in a
throwaway lawrence-api-style Express route on ai@6, point PostgresStore at a local
pool, register the Inngest workflow over connect(), and run one ask_user
suspend/resume turn end to end. That confirms the three unknowns at once — v6 compatibility, the
in-process embed, and the durable suspend/resume on our own substrate — and turns the pass-2 decision
from a vibe into evidence.