feat(documents): document_read tool + document-context analytics (offload PR-1) - #1137
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>
This was referenced Sep 16, 2026
Merged
Merged
Merged
…title formatGap drops a zero seconds remainder, so a 420s cache gap renders as "7m", not "7m 0s". The assertion was written against the latter and was the only red check on this PR. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
philmerrell
added a commit
that referenced
this pull request
Sep 17, 2026
Resolves the document-context offload stack (#1137-#1141, #1143) against the feedback work. Every conflict was both sides adding adjacent members to the same model, flag module, allowlist or interface, so the resolution keeps both, except FEEDBACK_ROW_PROJECTION's turn-class comment: PR-1 widened that projection to the full document-context field set, which already carries hasDocuments and documentDigests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
Off
develop@c28ccdbd(the compaction stack #1125 → #1128 → #1129 → #1131 → #1132 is merged, so nothing here is stacked). Spec:docs/specs/document-context-offload.md— PR sequence re-cut in §5, field design in §6.1, decisions in §8.Why
Attachment conversations are 11% of prod sessions and 31% of model spend. Of the 95 September sessions that peaked over 100k tokens, 25 had an attachment in their last three turns, several of them 1–3-turn sessions at 280k–530k that are one huge upload; a compaction cut cannot touch those (thresholds spec §3.6 keeps attachments out of scope on purpose). And a correctness bug: on restore,
_strip_document_bytesreplaces every inline document with a contentless placeholder, so a returning user's document is silently gone (14% same-file re-upload rate, 87% byte-identical). Four of five attachment sessions rebuild their agent every turn, so the loss fires on turn 2, not after an idle gap.The spec's PRs 1–3 are the fix and ship as a unit; this is PR-1, the retrieval primitive the digest (PR-2) and the rehydrated restore (PR-3) point at. The explicit change from the spec's original plan: the analytics that were PR-7 ship here, so the cost work (PR-4) is measured from the first day and its ship/widen-pinning/abandon decision is decidable from stored rows.
What
document_read(agents/builtin_tools/document_read_tool.py, service inapis/shared/files/document_read.py). Modelled on Strands'retrieve_offloaded_content: no args lists the session's readable documents;upload_id + page_rangereturns a PDF page range re-assembled server-side into a nativedocumentblock (text layer plus page images, not flattened text) with a unique Bedrock-safe name and a page-numbering note;upload_id + patterngreps the text layer and returns matching lines with page (PDF) or line/paragraph (text, DOCX) numbers;upload_idalone returns a page index for PDFs or bounded text for DOCX/text withoffsetcontinuation.max_pagesdefault 8, hard cap 20; matches capped; DOCX via a stdlibzipfile+ElementTreeextractor, PDF via the already-pinnedpypdfium2. No new dependency.SessionIndexquery, positive answers memoized). Never onenabled_tools: theworkspace_filescatalog key is granted to no prod role, so an RBAC gate would ship the recovery path dark.DOCUMENT_TOOL_IDSis recorded and deliberately not inINJECTED_TOOL_IDS. Kill switchDOCUMENT_READ_ENABLED=false.document_toolselement of_create_cache_key, so an agent cached before the first upload is never served without it, while an attachment session that could keep a warm agent still does (a veto would have pushed the ~19% warm attachment sessions onto the turn-2 strip). The gate is monotonic, so the key flips once, on the attach turn, when restored history has no document to lose; the resume path recomputes the same gate.document_readresults are exempt from the tool-result offloader (OFFLOAD_EXEMPT_TOOLSin the mixin plus the plugin'sshould_offload); offloading the slice the model just asked for would undo the read.C#row, next toprefixTokens/compactionEvents:hasDocuments,documentCount,documentTokens,documentDigests,documentsAttached,documentSlices,documentSliceTokens,documentMime, anddocumentReads {calls, pages, bytes}from a newAfterToolCallEventtally inContextLedgerHook.document_strippedcompaction-ledger event (documents, documentTokens) recorded by_strip_document_bytes, so the defect's reach is measured before PR-3 removes it;document_rehydrated/document_offload(withcacheGapSeconds) kinds registered for PR-3/PR-4.fullDocumentCalls,digestOnlyCalls,documentReadCalls,documentReadPages.DocumentRead/DocumentReadPages/DocumentReadBytesEMF inAgentCoreStack/Compactionwithmode/formatproperties.dataCoverage.documents); the SPA anatomy page shows adoc/digest/+Npbadge per call, a Documents line in the expanded row, and a consumption summary under the Attachments card.What this does not change
Tests
New:
tests/shared/test_document_read.py(hand-built multi-page PDFs and DOCX archives through realpypdfium2: page identity under re-assembly,max_pagesand hard cap, past-the-end clipping, pattern with page numbers, literal fallback for a bad regex, bounded index, DOCX offset continuation and paragraph numbers, text-family delegation, non-document refusal, listing and gate);tests/agents/builtin_tools/test_document_read_tool.py(factory, error surfacing, content-free EMF, routes gate incl. memo bound and fail-closed lookup, kill switch, cache-key element, offloader exemption);tests/agents/main_agent/session/test_document_context.py(footprint summary, strip event, ledgerdocumentReads);tests/apis/app_api/admin/costs/test_document_fields.py(row decode incl. Decimal, anatomy, profile rollups from rows and from the session row, write-side deltas). SPA spec extended for the new page helpers.Backend suite in this worktree: 8399 passed, 3 skipped, 15 failed + 8 files not collected — every one of the 23 is
ModuleNotFoundError: openpyxlin the local venv (pinned inpyproject.toml, not installed here; I did not install packages). None touch this change; CI's install will settle it. The SPA spec could not be executed locally for the same reason (docx-preview/pptx-previewmissing from the availablenode_modules), so the page changes are typechecked by CI only — please watch that job.What remains
PR-2 (digest at upload, persisted on
FileMetadata), PR-3 (restore rehydrates digest + handle;document_stripped→document_rehydrated), PR-4 (offload trigger, pinning, percentage rollout,document_offloadwith cache gap; age olddocument_readslices), PR-5 (truncation cache-live guard), PR-6 (aggregate inline-bytes guard), PR-7 (a content-free thumbs signal keyed on(sessionId, messageId)— no feedback surface exists today, so the outcome join the kaizen reviews keep asking for is designed but not buildable yet).🤖 Generated with Claude Code