Lawrence engineering · Decision doc · 11 June 2026

Court bundles, without the round-trip

Lawyers build roughly 3,700 court bundles a year in the legacy platform's wizard, and exactly zero in v3, because v3 has no bundler at all. The documents live in the case; the bundling happens outside it. This doc lays out four shapes for letting Lawrence curate, index and assemble bundles in chat, so we can pick one.

agents + platform-v3 four paths · none chosen yet status: draft

What the lawyer sees

The target experience, common to all four paths: the lawyer asks for a bundle in chat, Lawrence proposes contents from the case files, the lawyer edits the proposal in plain English, and a preview assembles with the index resolved against real page numbers. Nothing touches the case file until they confirm.

legal-os · matter / Patel v Patel
Hearing bundle
Lawrence

The problem

A court bundle is the paginated, indexed PDF of case documents a lawyer files before a hearing or to close a case: an index up front, every page numbered, contents in the order the court expects. The documents are already in Lawhive. The bundling isn't. Today a lawyer downloads everything, assembles the bundle in a local PDF tool, uploads the result back, and emails it on to the court.

This isn't a niche workflow, and we don't have to guess. The legacy platform has a manual bundling wizard, and its analytics say lawyers lean on it constantly:

~3,700
bundles saved in the last 12 months (legacy wizard)
~40
unique lawyers bundling per month
1.5×
previews generated per saved bundle: lawyers already iterate
0
bundling capability in v3 today

The wizard (built by George Gebbett, May 2024, still live at /solicitor/cases/[id]/bundles/create) merges hand-picked PDFs and images into one file with optional page numbers. What it doesn't do is the part that makes a bundle a court bundle: no index, no sections, no automatic ordering, no DOCX support, and encrypted PDFs are silently dropped. It's also whole files only: the tool's entire input is a list of file ids plus one page-numbering boolean, so there is no way to include just pages 3–9 of an exhibit. A lawyer who needs a partial document (the order without its cover letters, the signed page of an agreement) extracts it locally first. The lawyer does all the curation by drag-and-drop, then fixes the gaps locally anyway. And none of it was ever ported to v3, so every firm that migrates loses even that.

The judgment work, what goes in, in what order, under what index entry, is exactly what a static wizard can't do and an agent can.

That's the bet of this doc: don't port the wizard, and don't build a fancier one. The failure mode of bundling tools is prescriptiveness; every court, practice area and judge has its own expectations, and a form-based UI has to hardcode a shape. A conversation doesn't. Lawrence curates, orders and writes the index in dialogue with the lawyer; deterministic machinery converts, merges, stamps and resolves. The question this doc settles is only what shape the machinery's tool surface takes.

What's already on the shelf

A scan of platform-v3, agents and the legacy platform (June 2026). The encouraging part: most of the hard pieces exist somewhere already. The honest part: the two pieces in the middle of the feature don't exist in v3 at all.

Exists, reusable

  • Document storage + client visibility. MatterFile + S3, with a visibility flag the client portal already respects. A saved bundle is client-visible by flipping one field.platform-v3 · matter-service
  • DOCX → PDF conversion. LibreOffice-based converter, plus image and email conversion. Mixed-format inputs are mostly solved.platform-v3 · packages/pdf-converter
  • PDF merge + page stamping. Production-proven pdf-lib code in the legacy bundler, including the preview-bucket-then-copy save pattern. Portable.legacy platform · cases/files/bundles
  • Lawrence's tool surface. VFS read/create/edit, document creation, and a proven pattern for long-running agent work with progress in chat.agents · platform-vfs
  • Page-aware file reads. Lawrence reads PDFs natively as binary content blocks (DOCX via its PDF preview rendition), page structure intact, so it can reason about what sits on which page: the prerequisite for proposing "include only pages 3–9".platform-v3 · lawrence-api file adapter
  • Email with attachments. Nylas draft system. Once the bundle is a case file, emailing it to court is existing functionality.platform-v3 · email-service

Missing in v3

  • Any bundle concept. No model, no API, no UI. Only seeded test data referencing cases/…/bundles/*.pdf paths.
  • A PDF merge dependency. No pdf-lib or equivalent anywhere in the v3 stack.
  • Index / table-of-contents generation. Nowhere, including legacy. This is the piece lawyers fix by hand today.
  • Bookmarks and section structure. Bundles are flat concatenations or nothing.
  • Graceful handling of awkward inputs. The legacy wizard silently excludes encrypted PDFs; nothing surfaces the gap to the lawyer.

The shape we've already agreed

Four product decisions are settled from the brainstorm and constrain every path below. They're the fixed points; the paths only differ in how they implement them.

Primitives, not formatsNo hardcoded bundle format, no per-court templates. Lawrence composes the bundle, writes the index entries and adapts to the lawyer's or the court's directions conversationally. The machinery offers capabilities, not opinions.
The index is never guessedIndex page references are computed against the final merged pagination by the assembly engine. The agent supplies the words; the engine supplies the numbers. A bundle with a wrong index is worse than no bundle.
Preview, then confirmThis is a court-bound artifact. Lawrence assembles to a preview the lawyer can open and iterate on; nothing lands on the case until they confirm. Built on existing models (the legacy preview-bucket pattern), not a new draft-state entity.
v1 ends at a case fileThe confirmed bundle is an ordinary case file with the requested visibility. Client access, email to court, and anything else that consumes files ride existing rails. No bespoke distribution.

Four ways to hand Lawrence the scissors

All four paths share the same foundation: port the legacy merge and stamping code, reuse the converter, reuse preview-then-copy. In every path a bundle item can be a whole document or a page range of one (the partial-document need the legacy tool never met). They split into two families. A and B keep assembly in platform code the agent calls. C and D hand composition to the agent itself: as a chain of tool calls, or as a program it writes.

A

One declarative assembly call

Lawrence passes an ordered list of items, case documents plus any pages it authored itself (index, cover, dividers), in a single call. The engine converts, merges, stamps and resolves; the chat holds the working state.

agent curates in conversation, drafts index entries
assemble (items + options) → engine merges, paginates, resolves index
preview ↔ tweak = re-issue the call with an edited list
confirm → ordinary case file
  • Leanest build; no new entities anywhere
  • Index accuracy is the engine's job by construction
  • Evolves into B without a rewrite: the call's payload is the manifest
  • Bundle definition isn't durable; resuming next week means re-deriving from chat
  • Large bundles re-process fully on every tweak
Best for shipping v1 fast and validating that agentic bundling solves the real problem.
B

Persistent manifest, compiled

The same engine, but the bundle definition is a persisted artifact on the matter, riding existing artifact/document models. The agent edits the manifest; the engine compiles manifest to PDF, as many times as needed.

agent creates a manifest artifact on the matter
compile manifest → preview
edit + recompile ↔ durable across sessions and weeks
confirm → case file; manifest stays for re-issue
  • Durable, resumable, auditable definition
  • Re-issuing an updated bundle before a hearing is "add two items, recompile"
  • Same engine and index guarantees as A
  • The manifest becomes a first-class thing: schema, lifecycle, maybe UI
  • Schema churn while we're still learning what lawyers ask for
Best when bundles prove to be living, re-issued deliverables rather than one-shots.
C

Fine-grained PDF primitives

The inversion: merge, stamp pages, insert page, bookmark and page-count as independent tools the agent chains step by step in a working area. The agent performs the assembly, not just the curation.

agent converts each doc, tracks intermediates
mergepage-count → compute index refs itself
render indexinsertre-stamp → preview
confirm → case file
  • Maximum theoretical flexibility
  • Primitives reusable for other document workflows
  • The agent does page math: the one place accuracy must be deterministic
  • Intermediates need a workspace, colliding with "no new state holder"
  • Slow multi-call assembly; every step is a failure surface
Best almost never: if the agent should compose the assembly itself, path D delivers the same flexibility as one program instead of a call chain.
D

One generic tool: a code sandbox

Lawrence gets a sandboxed interpreter with a filesystem. Working copies of the case files are staged in; the agent writes a short Python program (against a vetted bundle library, with pypdf underneath) that assembles the whole bundle in one execution; the output comes back through the same preview-then-confirm publish step.

agent writes a driver script: the manifest, as code
sandbox runs it: convert, extract, merge, index, stamp in one pass
preview ↔ tweak = edit the script, re-run
confirm → ordinary case file
  • One generic tool instead of eight bespoke ones; flexibility without a new tool per need
  • Composition moves into a program: loops, asserts and one-pass page math, where models are strong
  • A platform capability with other customers waiting (form calculated fields is already named)
  • A sandbox platform to stand up: image, egress lockdown, scoped credentials, security review for client documents
  • Assembly code is generated per run unless a vetted library ships in the image
Best when code execution is wanted as a platform capability anyway; bundles become its second customer rather than the reason to build it.

The same request, four tool surfaces

What "build me a hearing bundle" turns into underneath, per path. Contracts are illustrative, not specs; the point is the shape, not the field names.

A · one call
assemble_bundle {
  title: "Hearing bundle",
  page_numbers: true,
  index: { placement: "front" },
  items: [
    { ref: "files://…/fa1",
      label: "Form A application",
      pages: "3-9" },
    { ref: "files://…/ord1",
      label: "Consent order · 14 Jan" },
    …
  ]
}
→ { preview, pages: 183,
    page_map, excluded: [] }
Working state lives in the call (and so in the conversation). A tweak re-issues the call with an edited list.
B · manifest + compile
create bundles://…/manifest
  { title, page_numbers,
    index, items: […] }

compile_bundle {
  manifest: "bundles://…/m1"
}
→ { preview, pages: 183,
    page_map }

# next week, two new orders:
edit bundles://…/m1
  { add: [ …two items… ] }
compile_bundle { … }
Same payload, but persisted on the matter and editable across sessions. Compile is repeatable by construction.
C · a chain of steps
pdf_convert  ×2 (the docx files)
pdf_extract { fa1, pages: "3-9" }
pdf_merge { parts: [6 refs] }
# agent maps labels → start pages
create index page (docs flow)
pdf_convert { index }
pdf_insert { at: 0 }
# index added 2 pages: every ref
# is now off by 2; agent recomputes,
# re-renders, re-inserts
pdf_stamp { … }
pdf_bookmarks { … }
publish_to_case_files { … }
Twelve-plus calls, intermediates to track, and the index arithmetic lands on the model. Fleshed out concretely below.
D · a program
run_code {
  files: [ "files://…/fa1", … ],   # staged to /case
  script: '''
from lawhive_bundle import Bundle

b = Bundle("Hearing bundle", page_numbers=True)
b.add("/case/fa1.pdf", "Form A application",
      section="A · Applications & orders",
      pages="3-9")
b.add("/case/ord1.pdf", "Consent order · 14 Jan",
      section="A · Applications & orders")
# … remaining items
b.index(placement="front")
b.save("/out/bundle.pdf")
'''
}
→ { preview, pages: 183, page_map }
The manifest became code. The index maths stays in a vetted library baked into the image; the agent writes only the driver. Drop to raw pypdf for requests the library never anticipated.

Side by side

A · declarative callB · persistent manifestC · primitivesD · code sandbox
Net-new machineryAssembly engine + 2 toolsEngine + manifest schema, lifecycle, edit tools7–8 PDF tools + a workspace protocolA sandbox platform + image + publish path
Working state livesIn the conversationOn the matter, as an artifactIn agent-managed intermediatesSandbox filesystem, ephemeral; the script in chat
Iterating on the previewRe-issue the callEdit manifest, recompileRe-run the chainEdit the script, re-run
Resume next weekRe-derive from chatOpen the manifestGoneRe-derive from chat
Re-issue before a hearingRe-derive, re-assembleAdd items, recompileFull re-runRe-run the script with additions
Partial documents (page ranges)A pages field per itemA pages field per itemAn extract step per rangeA slice in code
Index page referencesEngine-resolvedEngine-resolvedAgent arithmeticLibrary-resolved if a vetted helper ships; agent arithmetic on raw pypdf
"No new state holder" fitCleanDepends on host model choiceNeeds a workspaceClean: nothing outlives the run
Failure surfaceOne call to hardenOne call + manifest driftEvery step in the chainOne generated program per run

Path C, concretely

What the primitives approach would actually mean, in tools and state. Each tool is small and honest; the cost only becomes visible when you watch the agent thread them together.

ToolWhat it doesReturns
pdf_infoPage count, encryption status, dimensions of a case file or workspace refmetadata
pdf_convertDOCX / image / email to PDF via the existing converter; PDFs pass throughnew workspace:// ref + page count
pdf_extractPull a page range out of a document ("3-9", "12", "20-end")new ref + page count
pdf_mergeConcatenate refs in ordernew ref + per-part start pages
pdf_insertInsert one ref's pages into another at a position (the index, a divider)new ref
pdf_stampStamp page numbers (or re-stamp after an insert)new ref
pdf_bookmarksWrite the PDF outline from {title, page} pairsnew ref
publish_to_case_filesCopy a workspace ref into case files with filename + visibilitycase file id

The workspace is where every intermediate lives: concretely, a per-conversation S3 prefix behind a workspace:// VFS protocol, every tool taking refs in and returning a new immutable ref, stat/list so the agent can re-orient mid-task, and an S3 lifecycle rule that expires the lot after a few days. None of that is exotic. But it is a new state surface: the agent owns a directory of intermediates and must keep a dozen refs coherent across the chain, where in paths A and B the only state is one items list.

The index is where the chain bites. Inserting the index changes the page numbers the index refers to: a two-page index shifts every reference by two. The engine in A and B solves that once, in tested code (lay out the index, measure it, offset the page map, render). In C the same procedure lives in a skill prompt, executed by the model with tool calls: merge, read the start pages, write the index, convert it, insert it, notice the shift, recompute, re-render, re-insert, then re-stamp. Every step is individually trivial and the composition is exactly the kind of bookkeeping models fumble under load. For the six-document demo bundle the count is twelve-plus calls against A's two.

Where C genuinely shines: page surgery as a standalone need ("pull the signed page out of this agreement", "split this scan into three files"), and reuse, since each primitive is useful to other document workflows. But everything C is good at, D does better with one tool instead of eight. That's the next section.

The sandbox, concretely (path D)

The wariness about eight bespoke tools has a named answer in the current agent-tooling playbook: code mode. Instead of exposing each operation as a tool, expose one generic capability, sandboxed code execution with a filesystem, and let the agent write the program. Cloudflare published the pattern in September 2025; Anthropic's code-execution-with-MCP post validated it that November. The insight transfers exactly: models are far better at writing a sixty-line script (loops, asserts, one-pass page arithmetic) than at threading twelve stateful tool calls through a chat loop. C's whole failure mode, the index two-pass bookkeeping, becomes ordinary programming.

Determinism comes back through the image, not the prompt: bake a vetted lawhive_bundle helper library into the sandbox alongside pypdf. The agent writes the driver (the manifest, as code); the library computes index layout and page references in tested code. That is path A's engine delivered as a library instead of a service, with raw pypdf available underneath for requests the library never anticipated. The publish step stays on the platform side either way: the sandbox returns the PDF, and the existing preview-then-confirm flow registers it, so no sandbox ever holds broad S3 credentials.

Getting the files in and out

The data flow is the same whichever runtime wins, and it is the same contract as paths A and B: the tool call carries refs, never bytes and never credentials. lawrence-api resolves them, permission-checks them against the matter, copies only the curated subset (the six-to-fifteen items the agent chose, not the whole matter's files) to a throwaway staging prefix, and pushes those into the sandbox filesystem. Lawrence iterates in place against the staged copies; the single output PDF comes back through the preview, and only a confirm turns it into a case file.

PLATFORM · v3 trust zone your AWS / data boundary · credentials never leave here SANDBOX · ephemeral, walled no egress · no matter-bucket creds · microVM · dies on confirm/timeout Lawrence (chat) curates the bundle lawrence-api resolve refs · RBAC check · stage Matter files (S3) every MatterFile on the matter Staging prefix (S3) throwaway · lifecycle-expired /case — staged copies read-only · curated subset only driver script lawhive_bundle · pypdf · fpdf /out/bundle.pdf Preview bucket + signed URL Lawyer reviews confirm gate MatterFile (case file) visibility flag · email-ready 1 assemble_bundle · refs only 2 read curated items · RBAC 3 CopyObject — server-side, metadata-only; bytes skip lawrence-api 4 bytes in: inline push ≤100MB · or mount/pull iterate in place 5 one PDF out 6 on confirm
The staging contract, identical across every runtime (and the DIY Lambda): refs in, a permission-checked copy of the curated subset to a throwaway prefix, bytes pushed into a walled sandbox, one PDF back through the preview and confirm gate. The sandbox's whole reachable world is /case and /out — no egress, no matter-bucket credentials.

On the latency you flagged: the staging copy (step 3) is an S3 CopyObject, a server-side metadata operation, so it neither pulls bytes through lawrence-api nor scales with file size in any way you'd feel. The byte movement that actually costs wall-clock is getting files into the sandbox filesystem (step 4) and, more than that, the DOCX→PDF conversion inside — CPU-bound per document, and the same step that dominated the worked demo. Two things bound it: you stage only the curated subset, not the whole matter, and most bundles are a handful of already-PDF files where the whole flow is sub-second. The case to actually benchmark before committing is the large scanned matter (hundreds of MB), where inline push may lose to mount-and-pull — and that's a measurement, the same one, whichever runtime wins, not a guess to make now.

The real question is the runtime, and the options are not equal (state of play June 2026):

RuntimeRuns pypdf?Data boundaryVerdict for legal documents
Pydantic MontyNo, by designIn-process (Rust VM)Third-party libraries are an explicit non-goal (no classes, no zlib/io yet; v0.0.18). Right tool for safe in-process glue logic over host-injected functions; injecting PDF host functions would reinvent path C's toolbox in-process. Not a fit for assembly.
Vercel SandboxYes (pip install)us-east-1 onlyGA Jan 2026, Firecracker microVMs, pleasant SDK. But a single US region means client documents leave the UK/EU boundary. Hard sell without an Enterprise region story.
Cloudflare Sandbox SDKYes (own Dockerfile)Region pinning unverifiedGA Apr 2026, containers behind Durable Objects, and a genuinely good egress-proxy design (credentials injected per-host by a Worker, never visible to agent code). UK data-localisation for Containers needs confirming.
Bedrock AgentCore Code InterpreterYes, pre-installedInside our AWS account (VPC mode)Production, pypdf and friends pre-installed, S3 file flows to 5 GB, sandbox mode blocks all egress, VPC/PrivateLink mode keeps documents inside the existing AWS boundary. The natural fit for this stack: Code Interpreter is confirmed available in eu-west-2 (London) as of mid-2026, so the AWS-boundary story holds in the UK. Note the 2026 credential-exfiltration research (patched) as proof this class still needs a real security review.

What D actually costs is not the bundle feature, it's the platform: the image with its vetted library, egress lockdown, scoped credentials, observability for agent-written code, and a data-protection review before client documents enter any sandbox. That bill is bigger than everything in "what it costs to build" combined. The reason it might still be worth paying is that it isn't a bundle bill: the form-filling plan already names calculated fields via code execution as a fast-follow, and the long tail of document surgery ("split this scan", "pull the signed page") has no better home. Code execution is a platform bet with a queue of customers; bundles shouldn't be the reason to make it, and shouldn't wait for it either.

How you'd de-risk it cheaply. There is no local emulator for the Code Interpreter — it's cloud-only — so a "fully offline" test isn't possible, but a faithful one is: run the orchestration locally and call the real sandbox in a dev AWS account, on seeded matters only. Put the runner behind a swappable interface so a plain Docker container (network off, scoped mounts) covers the fast inner loop while the dev-account AgentCore call validates the things Docker can only fake — cold start, the file APIs and their limits, egress modes, real cost. Run that real-runtime test early, not last: it's what catches parity drift before the laptop conveniences are baked in. The only prerequisite the cloud path forces sooner is a light data-handling check before anything resembling client material enters a sandbox; the full review still gates real data.

Where this leans

A, with B as the named evolution and D as the named platform bet. A's call payload is already the manifest; persisting it later is additive, so B's schema decisions can wait until real usage shows what lawyers actually put in bundles and how often they re-issue them. The legacy data gives B's case some early weight (1.5 previews per save says iteration is real; whether cross-week re-issue is real, we can't see yet), but not enough to pay B's design cost up front. C is dominated by D: if the agent should compose the assembly itself, one program beats twelve chat-loop tool calls on every axis, so the eight-tool toolbox shouldn't be built in any future. The partial-document requirement moves nothing: it's a pages field on an item in A and B, a slice in code in D.

That leaves A versus D as the live decision, and it's a sequencing call, not a religious one. A ships bundles on existing infrastructure with court-bound determinism and nothing new to secure. D is the strategically interesting bet, but standing up a code-execution platform (runtime, image, egress, data-protection review) just to ship ~300 bundles a month is the most expensive possible way to get a bundle. And A forecloses nothing: its engine internals are exactly the vetted lawhive_bundle library a future sandbox would bake into its image. Build A now; if and when code execution lands as a platform capability, the engine moves into the image and the assemble call becomes one of several programs Lawrence can write.

One framing worth keeping even if it never becomes scope: the assembly engine is really a general "compile documents into a deliverable" primitive. Court bundles are the first user; exhibit packs, disclosure packs and closing packs are the same machinery with different curation. That's a reason to keep the engine's contract bundle-agnostic, not a reason to build any of those now.

The worked trace (path A)

The walkthrough at the top of the page, as the operations Lawrence would run. Contract shapes are illustrative. Note the password-protected file: the agent surfaces it conversationally instead of silently dropping it like the legacy wizard does. Under B, the items array would live in a manifest artifact instead of the call; under C, the assemble step explodes into the chain shown above; under D, it becomes a script written against the same engine, packaged as a library in the sandbox image.

Lawrence · tool calls matter mat_ptl · hearing bundle

What it costs to build

Shared foundation, whichever path wins:

  1. Port the merge. Bring the legacy bundler's pdf-lib merge and page-stamping into a v3 package, plus mixed-format input via the existing converter.
  2. Index resolution. The genuinely new piece: lay out agent-authored index entries with page references computed from the merge's page map.
  3. Preview plumbing. Preview bucket + signed URL + copy-to-case-files on confirm, lifted from the legacy pattern.
  4. Chat surfaces. Tool cards for assemble/confirm and a preview link the lawyer can open, following the existing tool-message components.

Then the per-path delta: A adds two tools and a skill. B adds everything A does plus the manifest as a designed object (schema, host model, edit operations, lifecycle). C adds five or six tools, workspace management, and a skill complex enough to do bookkeeping the other paths get for free. D adds a code-execution platform: runtime selection, the image with its vetted library, egress lockdown, scoped credentials, observability, and a data-protection review, none of which is bundle-specific.

What would change the decision

Sources: legacy bundler at packages/api/src/modules/cases/files/bundles (platform repo) and apps/platform/src/features/bundles; usage from PostHog project [prd] [v2] Platform, events document-bundle-created, bundle.createFromCaseDocs, bundle.copyToCaseDocs, 12 months to June 2026; v3/agents capability scan June 2026; sandbox-runtime research June 2026 (Monty, Vercel Sandbox, Cloudflare Sandbox SDK, Bedrock AgentCore Code Interpreter). Companion reading: agentic form filling and Lawrence file organisation, the two nearest shipped patterns.