previews · Architecture & design · First pass: the agent framework

Lawrence engineering · Implementation plan · June 2026

First pass: the agent framework

The implementation plan for the in-house TypeScript agent framework: a thin defineAgent module in lawrence-api on the Vercel AI SDK v6, proven by two deliberately small matter-scoped agents. A single-shot prompt-suggestions agent and a streaming lawrence-mini, together exercising both transports. Eleven units, three phases, and the framework ships inert until something invokes it.

target: lawrence-api both agents matter-scoped status: plan, ready to build

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).

Update · AI SDK v7 is the base (GA 2026-06-25). This first pass was built on v6; the base is now v7 (a breaking major). The framework shape is unchanged, but the implementation migrates: mostly mechanical renames (systeminstructions, stepCountIsisStepCount, experimental_outputoutput, provider-registry types) via npx @ai-sdk/codemod v7, and the durable-runs and HITL primitives we deferred now ship in-SDK. See the migration doc (v7 update) for the base decision and the build-time gates.

This operationalises the strategy in The loop, in TypeScript and its framework shape sketch. It is the first concrete step on One loop, many Lawrences: enough framework to run two small agents, nothing more. The plan markdown lives in platform-v3/docs/plans/.

3
phases, dependency-ordered
11
implementation units
2
small agents, both transports
v6
AI SDK, pinned latest first

What we're building

The agent loop runs in Python today, and lawrence-api only proxies its stream. This first pass moves just enough of the runtime to TypeScript to prove the shape: a defineAgent({ ... }) surface, a slug registry, Langfuse prompt resolution, an initialContext hook for server-side context assembly, structured output, and two transports off one runtime: single-shot run (JSON) and streaming chat (SSE). The VFS gets a thin slice that reuses the existing matter-scoped adapters through a generic-verb layer.

It is deliberately small. Two real-but-bounded agents validate the framework and the defineAgent ergonomics; the harder capabilities (the matter-intake agent, non-matter scope, the full VFS catalogue) land later on validated foundations rather than co-evolving with an unproven framework.

Prove the spine with two small agents, then the rest of “many Lawrences” builds on something that already runs.

The two agents

One per transport. Both matter-scoped, so this pass needs no new scope machinery: the existing matter auth chain and the existing matter-scoped VFS adapters carry the weight.

prompt-suggestions run · single-shot

Suggests prompts the lawyer could send Lawrence, from the matter's case context. Proves the spine: registry, prompt resolution, the initialContext hook, and structured output, with no tools and no streaming.

transportrun → JSON
contractstructured (generateObject)
contextinitialContextgetCaseContext
tools / vfsnone

lawrence-mini stream · chat

A stripped-down matter-scoped Lawrence. Proves the streaming transport, the tool loop, and the VFS-as-tools surface. No citations, no full tool catalogue, stateless in this pass.

transportstream → SSE
contracttext (streamText)
vfs slicedocuments, files · read, list
loop boundstopWhen: stepCountIs(n)

The build, in three phases

Eleven units, dependency-ordered. Phase A is the spine, Phase B ships the first working agent, Phase C adds streaming, the VFS slice, and the second agent.

phaseAfoundation

The framework spine

Everything the runtime needs before an agent can run, with no behaviour shipped to users.

U1Pin AI SDK to latest 6.0.x. Isolated bump off the lockfile-pinned 6.0.132 so framework code is written against current, not deprecated, APIs.
U2Provider registry. Resolve a model id to a direct lab provider; never the default Vercel gateway.
U3defineAgent + registry + types. The authoring surface and the slug lookup.
U4Langfuse prompt resolution. Fetch by name + label, compile variables, model can come from prompt config.
U5Context + initialContext hook. Resolve scope from auth, run the hook into render variables.
phaseBfirst agent

Single-shot, end to end

The run transport plus the first agent. After this phase a working agent exists.

U6Runtime run path + route. POST /agents/:slug/run, structured output via generateObject, built so an agentic-then-structured agent reuses it later via Output.
U7prompt-suggestions agent. CCO via initialContext reusing getCaseContext; structured suggestions.
phaseCstream + vfs

Streaming, the VFS slice, the second agent

The streaming transport, the generic-verb VFS layer, and the matter-scoped chat agent that uses both.

U8Runtime stream path + route. streamText + stopWhen, reusing the house stream-adapter; Anthropic cache discipline.
U9VFS thin slice. VfsConfig-gated read/list tools dispatching to existing matter-scoped adapters via the vfs-registry.
U10lawrence-mini agent. Streaming, matter-scoped, the thin VFS slice over documents and files.
U11Feature-flag gate. Server-side PostHog check on the endpoints, fail-closed; the legal-os UI flag reserved for later.

Phase A + B alone ships a working agent — the streaming surface is additive.

The decisions that shape it

The load-bearing calls, each settled with the team before the plan landed.

DecisionWhy
Compose streamText / generateObject, not ToolLoopAgentThe loop already lives in streamText; v6's ToolLoopAgent wraps it without adding anything we need. Composing the core functions keeps control over the two transports, structured output, later MCP injection, and Langfuse spans.
Direct lab providers, never the gatewayA bare model string routes through the Vercel AI Gateway (US, Vercel-credentialed) by default. Resolution is a framework-internal seam: agent authors give a model id, the resolver maps it to a direct provider now and to a self-hosted LiteLLM-style proxy later, with no change to any agent definition.
Transport and contract are independent axesTransport (run / stream), contract (text / object) and tools are orthogonal. Structured-without-tools uses generateObject; structured-with-tools (agentic context building, then a typed object) uses generateText/streamText + Output. Pass 1 exercises two combinations; matter-intake (agentic, then structured) drops in later without reworking the loop.
VFS slice reuses the matter-scoped adaptersThe framework adds the VfsConfig + generic-verb tool surface and dispatches in-process to the existing vfs-registry adapters by matterId. No change to the matter-bound @lawhive/content-framework contract; the non-matter scope-climb is a separate follow-up.
initialContext is an explicit seamPython has no such hook (context assembly is inline in a 1600-line method). The TS hook returns an assembled context object; a render step maps it into Langfuse variables. The CCO fetch reuses the existing getCaseContext service, not a reimplementation.
Bump the AI SDK first, isolated and pinnedThe bare caret plus a frozen dist-tag is why the lockfile sits at 6.0.132 (latest is 6.0.208). Bumping first lets the framework be written against the current streaming API, not the already-deprecated names.

What this pass does not do

Deliberately deferred, so the spine lands clean.

non-matter scope & the scope-climb matter-intake / matter-creation full VFS namespace catalogue presets / builder create / edit verbs citation processing thread persistence surface-based tool filtering Python agent-gateway cutover durable execution + durable streams code execution (AgentCore) token-budget loop control
Coexistence

The framework and agent definitions ship to production inert: registered, but nothing invokes them until deliberately wired, and only agent-invoking UI is feature-flagged. The Python agent-gateway and the existing /lawrence/chat route are untouched.

Top risks

RiskSeverityMitigation
AI SDK streaming surface mid-renameMediumU1 bumps first; U8 builds against the current names (.stream, createUIMessageStreamResponse).
ai bump blast radiusMediumOther workspaces import ai (legal-os). U1 is isolated and gated on repo-wide pnpm check.
Anthropic cost without cache disciplineHighKeep per-turn variables out of the system prompt; reproduce cache breakpoints on the streaming agent. Verify cost against the Anthropic console, not Langfuse.
ANTHROPIC_API_KEY env wiringMediumDoppler + Parameter Store + Flightcontrol; prod containers only get vars referenced in Flightcontrol config.
Route collision on /agents/chatMediumThe legacy proxy already owns that path. Namespace the new routes (/agents/:slug/run, /agents/:slug/chat).

One line: build a thin defineAgent framework in lawrence-api on the AI SDK v6 (compose the primitives, not ToolLoopAgent; direct lab providers, not the Vercel gateway), prove it with two small matter-scoped agents across three phases, reuse the existing matter-scoped VFS adapters, and defer the non-matter scope-climb. Phase A + B ships a working agent; the framework sits inert until wired.