previews · Architecture & design · Mastra harness vs our framework

Lawrence engineering · Evaluation · June 2026

Mastra's Agent Harness: borrow, adopt, or build?

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.

same base: AI SDK library, not a deployment state on our DB · durable on Inngest

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.

CapabilityWhat it does
SessionsPersistent multi-turn threads, keyed by threadId, written to a storage backend; create / switch / rename / clone.
Observational memoryCompresses long runs via observer → reflector agents (observations merged) instead of lossy summarisation.
Steer & interruptsteer(), follow-up queuing — a conversation you can watch, interrupt and redirect mid-run.
Tool approvalsPer-tool / per-category allow/deny; a built-in ask_user that suspends the run; respondToToolApproval().
SubagentsDelegation to subagents exposed as a tool — isolated (fresh context) or forked (cloned parent conversation, warm cache).
ModesSpecialised 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 layerVercel AI SDK v6, directVercel AI SDK, under the hood — same
Author surfacedefineAgent({...}), thin & oursAgent + Harness classes
Lives inlawrence-apilawrence-api (library) — same
Transportsrun + streaming (UI message stream)event stream + displayState; resumable via DurableAgent
Sessions / persistencedeferred stateless in pass 1built-in our Postgres via PostgresStore
Memory / compactiondeferredbuilt-in observational memory
Durable long runsdeferred (the durable-streams PoC)native DurableAgent + Inngest runner
Subagentsroadmap (“many Lawrences”)built-in isolated / forked
Modes / handoffnot built (≈ our surface idea)built-in transitionsTo
Human-in-the-loopnone control via VFS exposure, writes are draftsbuilt-in ask_user suspend + approvals
Data accessour VFS over @lawhive/content-framework, our services, auth, LangfuseMastra 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.

AWS eu-west-2 · self-hosted · UK/EU residency legal-os stream · reconnect lawrence-api · TypeScript (one service) agents routes · auth → flag → matter POST /agents/:slug/chat Harness / DurableAgent @mastra/core stream · observe · resume Mastra workflow steps @mastra/inngest createStep → Inngest step our tools = VFS over content-framework + services matterId / identityId from request context + existing Inngest Connect (connect()) Inngest Cloud orchestration (metadata, US) Postgres (ours) sessions · memory shared pg.Pool Redis PubSub resumable streams request SSE · observe()/replay connect() snapshots events Maps onto what we deferred • Durable streams (client disconnect/reconnect) → DurableAgent PubSub + cache • Human-in-the-loop / approval → workflow suspend, resumed by an Inngest wait • Long-run memory + sessions → observational memory, persisted to our Postgres Compute + state stay in-region; only Inngest Cloud orchestration metadata is US — the same exposure as today.
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.

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.