feat(documents): DocumentDigest built at upload and persisted on FileMetadata (offload PR-2) - #1138
Merged
Merged
Conversation
…load PR-1) The recovery half of docs/specs/document-context-offload.md, with the analytics pulled forward from PR-7 so the cost work is measured from day one. - document_read (agents/builtin_tools/document_read_tool.py, service in apis/shared/files/document_read.py): PDF page ranges re-assembled into a native document block (max_pages 8, hard cap 20), regex over the text layer with page/paragraph numbers, bounded text for DOCX (stdlib extractor) and text-family files, a session listing and a PDF page index. Gated on the session having a readable upload, never on enabled_tools; the id stays out of INJECTED_TOOL_IDS; DOCUMENT_READ_ENABLED=false removes it. - Tool presence rides the agent cache key (document_tools) instead of vetoing the cache, so an attachment session that keeps a warm agent still does; resume recomputes the same gate. - document_read results are exempt from the tool-result offloader. - Per-call document context on C# rows (hasDocuments, documentCount, documentTokens, documentDigests, documentsAttached, documentSlices, documentSliceTokens, documentMime, documentReads), a document_stripped compaction-ledger event from _strip_document_bytes, session rollups (fullDocumentCalls, digestOnlyCalls, documentReadCalls, documentReadPages), content-policy projections, anatomy + profile fields, SPA anatomy rendering, and DocumentRead EMF in AgentCoreStack/Compaction. - Spec: PR sequence re-cut (analytics in PR-1, outcome signal as PR-7), §6.1 field design, §8 decision log. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…Metadata (offload PR-2) Stacked on PR-1. The digest a later turn will carry instead of the document's bytes (docs/specs/document-context-offload.md §4A), built once, off the model path, when a document upload completes. - apis/shared/files/document_digest.py: a deterministic outline (headings with page/paragraph/line anchors, table and figure mentions, counts, a text sample spread across the document) via pypdfium2 and the PR-1 DOCX extractor, plus a 3-5 sentence abstract from Nova Micro (DOCUMENT_DIGEST_MODEL_ID) that fails open. render_digest emits the <document-digest> block under a hard 1,500-token budget (sections dropped first, then the abstract; the handle always survives) and the estimate is stored as digest.tokens. - FileMetadata.digest + FileUploadRepository.update_file_digest; complete_upload schedules the build as a strong-referenced background task for document uploads only. DOCUMENT_DIGEST_ENABLED=false skips it. - Content policy: digest.abstract / digest.sections denylisted; status, format, count and tokens projected; the attachment profile reports digested count and digest tokens. DocumentDigestGenerated/Tokens/Ms EMF in AgentCoreStack/Compaction. - Nothing in the chat path reads the digest yet (PR-3). Spec: PR-2 row marked built; §8 decisions 12-15. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This was referenced Sep 16, 2026
Merged
philmerrell
changed the base branch from
feature/document-offload-pr1
to
develop
September 17, 2026 00:37
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #1137 (PR-1). Merge in order; this PR's diff is the one commit on top. CI runs only for PRs targeting
develop/main, so this one shows no checks until PR-1 merges and it is re-targeted — the suite result below is the local run.Why
The offload design (spec §4) hands every turn after the attach turn a digest plus a
document_readhandle instead of the document's bytes. PR-1 built the handle. This builds the digest: once, at upload, off the model path, so PR-3 can rehydrate a restored document as a digest instead of today's contentless placeholder and PR-4 can swap an inline document for one without paying for extraction on a chat turn.What
DocumentDigest(apis/shared/files/document_digest.py), two parts on purpose:#) with the unit they start on, table and figure mentions, character count, and a text sample spread across the document. PDF text via the already-pinnedpypdfium2, DOCX via PR-1's stdlib extractor, text family from the bytes (HTML tags stripped). A document with no detectable headings gets an outline sampled from page first-lines; outlines past 40 entries are sampled evenly.DOCUMENT_DIGEST_MODEL_ID) over the sample plus outline — the same text model the tool-batch summaries and compaction summary use, so no new model access. A model error or a truncated generation leaves the digest with an outline and no abstract, never without an outline.render_digestproduces the<document-digest name upload_id format pages tables figures>block PR-3 will put in context, under a hard budget (DOCUMENT_DIGEST_MAX_TOKENS, 1,500): sections dropped from the end first, then the abstract truncated, the opening tag (thedocument_readhandle) always kept. The estimate is stored asdigest.tokens, so the ceiling is a stored fact per file.FileMetadata.digestas a plain map (FileUploadRepository.update_file_digest,SET digest, tolerant of a row deleted mid-build).complete_uploadschedules the build as a strong-referenced fire-and-forget task for document uploads only (not spreadsheets, decks or images); the upload response does not wait.DOCUMENT_DIGEST_ENABLED=falseskips it.digest.abstractanddigest.sectionsare denylisted (model prose and heading text);digest.status/format/count/tokensare projected, and the admin attachment profile reportsdigestedanddigestTokensso coverage and the per-file cost are visible without reading a document.DocumentDigestGenerated/DocumentDigestTokens/DocumentDigestMsinAgentCoreStack/Compactionwithformatandoutcome(ready/no_abstract/failed) — the p95 extractor latency the spec's gate asks for is read from here in dev.What this does not change
digestis absent — uploads that predate this PR and agent-written files (workspace_write, the Word / Excel / PowerPoint tools register rows directly) have none.bedrock:InvokeModel(title generation, api-converse).Tests
tests/shared/test_document_digest.py(heading heuristic incl. the one-word-paragraph case, PDF/DOCX/markdown/HTML outlines with anchors and table/figure counts, first-line fallback, even sampling past the cap, cross-document text sample, the rendered block's shape and escaping, budget trimming order, a 200-page PDF digest under 1,500 tokens, the abstract call's model id and prompt, truncated/failed generations,build_digestfail-open on extraction and abstract, class-name-only errors, content-free EMF, kill switch);tests/apis/app_api/test_file_digest_scheduling.py(documents only, task lifecycle, build/S3/repository failures never escape, no-loop no-op);update_file_digestround trip on the moto repository.Local backend suite (with a stub
openpyxlonPYTHONPATHfor verification only, since this venv lacks the pin; nothing committed): 8866 passed, 3 skipped; the only 33 failures are the two spreadsheet-preview test files, which need the realopenpyxlthe stub cannot stand in for. CI's install settles those.🤖 Generated with Claude Code