@abloatai/ablo 0.57.0 → 0.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/AGENTS.md +10 -4
  2. package/CHANGELOG.md +199 -13
  3. package/README.md +2 -1
  4. package/dist/ai-sdk.d.ts +1 -1
  5. package/dist/ai-sdk.d.ts.map +1 -1
  6. package/dist/context/evidence.d.ts +6 -8
  7. package/dist/context/evidence.d.ts.map +1 -1
  8. package/dist/context/evidence.js +6 -20
  9. package/dist/context/evidence.js.map +1 -1
  10. package/dist/context/index.d.ts +23 -0
  11. package/dist/context/index.d.ts.map +1 -0
  12. package/dist/context/index.js +26 -0
  13. package/dist/context/index.js.map +1 -0
  14. package/dist/context/onChange.d.ts +9 -0
  15. package/dist/context/onChange.d.ts.map +1 -0
  16. package/dist/context/onChange.js +37 -0
  17. package/dist/context/onChange.js.map +1 -0
  18. package/dist/source-conformance.d.ts +1 -1
  19. package/dist/source-conformance.d.ts.map +1 -1
  20. package/dist/source-conformance.js +1 -1
  21. package/dist/source-conformance.js.map +1 -1
  22. package/dist/source-drizzle.d.ts +1 -1
  23. package/dist/source-drizzle.d.ts.map +1 -1
  24. package/dist/source-drizzle.js +1 -1
  25. package/dist/source-drizzle.js.map +1 -1
  26. package/dist/source-kysely.d.ts +1 -1
  27. package/dist/source-kysely.d.ts.map +1 -1
  28. package/dist/source-kysely.js +1 -1
  29. package/dist/source-kysely.js.map +1 -1
  30. package/dist/source-next.d.ts +1 -1
  31. package/dist/source-next.d.ts.map +1 -1
  32. package/dist/source-next.js +1 -1
  33. package/dist/source-next.js.map +1 -1
  34. package/docs/agent-integration-decision-guide.md +123 -0
  35. package/docs/agents.md +18 -14
  36. package/docs/api-keys.md +6 -6
  37. package/docs/api.md +52 -29
  38. package/docs/branch-development.md +23 -4
  39. package/docs/cli.md +16 -9
  40. package/docs/client-behavior.md +21 -15
  41. package/docs/concurrency-convention.md +67 -77
  42. package/docs/context.md +56 -31
  43. package/docs/coordination.md +115 -36
  44. package/docs/data-sources.md +12 -6
  45. package/docs/debugging.md +1 -1
  46. package/docs/examples/agent-human.md +6 -18
  47. package/docs/examples/coordination-conformance.md +69 -0
  48. package/docs/examples/existing-document-pipeline.md +488 -0
  49. package/docs/examples/existing-python-backend.md +10 -13
  50. package/docs/examples/nextjs.md +2 -2
  51. package/docs/examples/scoped-agent.md +18 -1
  52. package/docs/examples/server-agent.md +2 -2
  53. package/docs/groups.md +19 -139
  54. package/docs/guarantees.md +5 -6
  55. package/docs/identity.md +2 -1
  56. package/docs/index.md +5 -0
  57. package/docs/integration-guide.md +20 -19
  58. package/docs/integrations/sandbox-runtime.md +148 -0
  59. package/docs/integrations.md +9 -0
  60. package/docs/operating-on-your-database.md +7 -0
  61. package/docs/quickstart.md +19 -13
  62. package/docs/react.md +9 -9
  63. package/docs/schema-contract.md +14 -13
  64. package/docs/sessions.md +1 -1
  65. package/examples/README.md +2 -2
  66. package/examples/agent-turn.ts +1 -1
  67. package/examples/expensive-agent-turn.ts +1 -1
  68. package/llms.txt +22 -11
  69. package/package.json +6 -6
  70. package/dist/context/sources.d.ts +0 -21
  71. package/dist/context/sources.d.ts.map +0 -1
  72. package/dist/context/sources.js +0 -36
  73. package/dist/context/sources.js.map +0 -1
  74. package/dist/context.d.ts +0 -22
  75. package/dist/context.d.ts.map +0 -1
  76. package/dist/context.js +0 -33
  77. package/dist/context.js.map +0 -1
@@ -0,0 +1,69 @@
1
+ # Verify hosted coordination separately
2
+
3
+ > Prove claim behavior once, without coupling the proof to a document, workflow, or GraphQL schema.
4
+
5
+ Use two test layers when adopting Ablo behind an existing application:
6
+
7
+ | Proof | Responsibility |
8
+ |---|---|
9
+ | Domain contract | State transitions, stale evidence, provenance, idempotency, and old/new path parity. |
10
+ | Hosted coordination conformance | Participant identity, exclusion, heartbeat, release, and lease-expiry recovery. |
11
+
12
+ The runnable hosted proof is
13
+ [`examples/coordination-conformance`](../../../../examples/coordination-conformance/README.md).
14
+ It creates a temporary test branch that inherits an existing non-production
15
+ schema. It does not push a schema or change domain rows.
16
+
17
+ ## File structure
18
+
19
+ ```text
20
+ src/conformance/index.ts
21
+ -> src/conformance/claimExclusion.ts
22
+ -> src/conformance/contract.ts
23
+ src/runtime/index.ts
24
+ -> src/runtime/client.ts
25
+ -> src/runtime/config.ts
26
+ live/index.ts
27
+ -> live/claimAndExit.ts
28
+ ```
29
+
30
+ The conformance operation depends on a narrow structural claim interface. The
31
+ runtime child supplies the real Ablo model resource. Domain examples depend on
32
+ the same narrow behavior without inheriting this runner's branch or credential
33
+ setup.
34
+
35
+ ## Run
36
+
37
+ First run the deterministic structure and configuration checks:
38
+
39
+ ```bash
40
+ cd examples/coordination-conformance
41
+ npm test
42
+ npm run typecheck
43
+ ```
44
+
45
+ Then name an existing model on the CLI login's dedicated non-production
46
+ project:
47
+
48
+ ```bash
49
+ ABLO_CONFORMANCE_MODEL=existingModel npm run test:live
50
+ ```
51
+
52
+ The model name supplies only a typed claim namespace. Every claim target is a
53
+ new random identifier; the proof creates no model row. The disposable branch
54
+ is deleted even when an assertion fails.
55
+
56
+ The runtime deliberately calls `model.claim(id, options)`. This identifier
57
+ overload is row-free. `model.claim({ id, ...options })` is the row-backed form:
58
+ it reads the model row and is not interchangeable in a coordination-only
59
+ rollout.
60
+
61
+ ## What this lets domain examples omit
62
+
63
+ A document-processing example does not need its own branch provisioning,
64
+ session delegation, heartbeat, or process-death fixture. It must still test its
65
+ own behavior when a claim is won, skipped, released after failure, and combined
66
+ with changing evidence.
67
+
68
+ This separation prevents a vertical testcase from force-replacing an inherited
69
+ schema merely to re-prove generic lease behavior.
@@ -0,0 +1,488 @@
1
+ # Process an existing document once
2
+
3
+ > This is an advanced evidence-backed state reference, not the default Ablo
4
+ > integration tutorial. If you are coordinating an operation that already owns
5
+ > its Postgres write, start with the
6
+ > [Agent Integration Decision Guide](../agent-integration-decision-guide.md).
7
+
8
+ > Coordinate expensive processing over an application-owned document without taking ownership of uploads or storage.
9
+
10
+ This TypeScript example begins with a stable document identifier. The existing
11
+ application service resolves that identifier to the current source version and
12
+ remains authoritative for authorization, database locking, idempotency, and the
13
+ processing result. Ablo selects one participating worker before whole-document
14
+ extraction and can coordinate individual processing-run page rows independently.
15
+
16
+ The complete runnable example is
17
+ [`examples/existing-document-pipeline`](../../../../examples/existing-document-pipeline/README.md).
18
+
19
+ ## File structure
20
+
21
+ ```text
22
+ src/documents/index.ts
23
+ -> src/documents/schema.ts
24
+ -> src/documents/processDocument/index.ts
25
+ -> src/documents/processDocument/existingPath.ts
26
+ -> src/documents/processDocument/coordinatedPath.ts
27
+ -> src/documents/processDocument/contract.ts
28
+ -> src/documents/processDocument/sourceChanged.ts
29
+ -> src/documents/coordination/index.ts
30
+ -> src/documents/coordination/client.ts
31
+ -> src/documents/coordination/contract.ts
32
+ -> src/documents/fields/index.ts
33
+ -> src/documents/fields/mapExtractResult.ts
34
+ -> src/documents/fields/fieldIdentity.ts
35
+ -> src/documents/fields/contract.ts
36
+ -> src/documents/pages/index.ts
37
+ -> src/documents/pages/claimPage.ts
38
+ -> src/documents/pages/mapParseResult.ts
39
+ -> src/documents/pages/pageIdentity.ts
40
+ -> src/documents/pages/contract.ts
41
+ -> src/documents/search/index.ts
42
+ -> src/documents/search/buildSearchProjection.ts
43
+ -> src/documents/search/planSearchProjectionPublication.ts
44
+ -> src/documents/search/searchDocuments.ts
45
+ -> src/documents/search/inMemoryBackend.ts
46
+ -> src/documents/search/searchIdentity.ts
47
+ -> src/documents/search/contract.ts
48
+ -> src/documents/review/index.ts
49
+ -> src/documents/review/addAnnotation.ts
50
+ -> src/documents/review/requestReview.ts
51
+ -> src/documents/review/signOffReview.ts
52
+ -> src/documents/review/abloRepository.ts
53
+ -> src/documents/review/reviewIdentity.ts
54
+ -> src/documents/review/contract.ts
55
+ ```
56
+
57
+ Enter through `src/documents/index.ts`. The processing operation and its
58
+ failure contract live below that owner. Ablo client construction and the claim
59
+ namespace live below `src/documents/coordination/index.ts`; they do not become a
60
+ generic file service.
61
+
62
+ The documented public operations are `createDocumentOperations`, `claimPage`,
63
+ `mapFullParseResult`, `mapPdfExtractResultWithCitations`,
64
+ `createSearchDocumentsOperation`, `planSearchProjectionPublication`,
65
+ `createDocumentReviewOperations`, and `createAbloReviewAdapter`.
66
+
67
+ `structure.json` declares the exact TypeScript inventory, those documented
68
+ operations, and the permitted cross-capability dependency edges. The structure
69
+ test rejects missing or unexpected files, missing documented operation exports,
70
+ undeclared lateral imports, and dependency cycles. This reduces structural
71
+ drift; it does not validate the meaning of every prose description.
72
+
73
+ ## Source pages and processing-run evidence
74
+
75
+ The metadata graph is:
76
+
77
+ ```text
78
+ documents
79
+ ├── documentPages stable source pages
80
+ └── documentProcessingRuns
81
+ ├── documentProcessingRunPages claimable output pages
82
+ │ └── documentBlocks run-owned parse evidence
83
+ ├── documentExtractionRuns
84
+ │ └── documentExtractedFields
85
+ │ └── documentFieldCitations references block + source page
86
+ ├── documentSearchProjections
87
+ │ └── documentSearchEntries versioned projection targets
88
+ ├── documentAnnotations recorded body + retained target
89
+ └── documentReviewIssues durable review state
90
+ └── documentReviewEvents operations append decisions
91
+ ```
92
+
93
+ Do not combine a source page with one parser run's output page. A source page is
94
+ identified by document ID, source version, and source page number. A run page
95
+ links that stable page to a processing run and records the output position for
96
+ that invocation. Blocks belong to the run page, so rerunning the same source
97
+ does not overwrite prior evidence.
98
+
99
+ The parent ownership path is document → processing run → run page → block. The
100
+ run page also carries direct document and source-page references for scoped
101
+ reads and provenance.
102
+
103
+ These models describe application-owned tables or projections exposed through
104
+ Ablo; they do not transfer migration authority to an agent. The existing
105
+ application owns their DDL, foreign keys, constraints, authorization, and
106
+ backfill. `fk: true` documents the expected connected-database contract.
107
+
108
+ The parser adapter is vendor-neutral, but its input deliberately matches the
109
+ useful parts of Reducto's current Parse response:
110
+
111
+ | Parser response | Local evidence |
112
+ |---|---|
113
+ | `usage.num_pages` | `documentProcessingRuns.processedPageCount` |
114
+ | `bbox.page` | `documentProcessingRunPages.outputPageNumber` |
115
+ | requested page manifest / `bbox.original_page` | `documentPages.sourcePageNumber` |
116
+ | block `type` and `content` | `documentBlocks.blockType` and `content` |
117
+ | normalized `left`, `top`, `width`, `height` | block geometry |
118
+ | categorical and granular confidence | block confidence fields |
119
+
120
+ Reducto documents blocks as atomic page elements with normalized `[0,1]`
121
+ bounding boxes, one-indexed processed and original page numbers, and parse or
122
+ extract confidence. The two page numbers differ when a page range is parsed.
123
+ [Reducto Parse response format](https://docs.reducto.ai/parse/response-format).
124
+
125
+ `mapFullParseResult` accepts only an inline response whose `result.type` is
126
+ `full`. Resolve a URL-backed result before calling it. The adapter also requires
127
+ the ordered source-page manifest used for the request. This preserves blank and
128
+ noncontiguous source pages without guessing an offset from returned blocks. The
129
+ processing run records `processorName` and `processorVersion`; preserve provider
130
+ block types at this adapter boundary and normalize them later only through an
131
+ explicit, versioned domain mapping.
132
+
133
+ Do not assign `chunks[].content` to a page by default. Reducto chunks group
134
+ blocks according to the selected chunking strategy and only become one chunk
135
+ per page when page chunking is requested.
136
+ [Reducto chunking methods](https://docs.reducto.ai/configs/parse/chunking-methods).
137
+
138
+ Extracted values can carry multiple citations with their own bounding boxes,
139
+ source content, parent block, and parse/extract confidence. Keep those as a
140
+ field-to-evidence relation rather than flattening them into the page row.
141
+ [Reducto Extract citations](https://docs.reducto.ai/extract/response-format).
142
+
143
+ Persist parser URLs only when the provider guarantees they are durable. Signed
144
+ or temporary image URLs are retrieval details, not provenance. Likewise,
145
+ `documentBlocks.content` and `documentExtractedFields.valueJson` are illustrative
146
+ evidence: applications handling sensitive documents should synchronize only
147
+ content an agent may receive, or retain a durable evidence reference and
148
+ geometry instead.
149
+
150
+ ## Extracted fields and citations
151
+
152
+ An extraction run records the extractor and extraction-schema versions. Fields
153
+ belong to that run and use RFC 6901 JSON-pointer paths such as `/total` and
154
+ `/line_items/0/amount`. This makes nested array items addressable without
155
+ turning dynamic field names into database columns. `valueJson` preserves the
156
+ typed provider value while `valueType` supports safe projection and display.
157
+
158
+ `mapPdfExtractResultWithCitations` recursively maps the citation-enabled
159
+ `{ value, citations }` shape. It is intentionally a PDF/image adapter:
160
+ spreadsheet citations use cell coordinates and require a separate adapter.
161
+
162
+ Every accepted citation must resolve uniquely through:
163
+
164
+ ```text
165
+ extracted field
166
+ -> field citation
167
+ -> processing-run page
168
+ -> run-owned parse block
169
+ -> stable source page
170
+ ```
171
+
172
+ The adapter rejects unwrapped leaves, field-count mismatches, non-JSON values,
173
+ page-manifest mismatches, invalid normalized geometry or confidence, and
174
+ orphaned or ambiguous block evidence. This is stricter than merely retaining a
175
+ provider citation blob: an agent can follow typed relations to the exact source
176
+ page and rectangle used for its conclusion.
177
+
178
+ ## Grounded document search
179
+
180
+ Search is a rebuildable projection, not another source of document truth.
181
+ `buildDocumentSearchProjection` creates two entry kinds behind one contract:
182
+
183
+ This is the query-side read-model separation described by the CQRS pattern;
184
+ the projection can be regenerated from the authoritative evidence records.
185
+ [Microsoft CQRS pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs).
186
+
187
+ | Entry kind | Searchable text | Required evidence |
188
+ |---|---|---|
189
+ | `parse_block` | block type and content | block → run page → source page |
190
+ | `extracted_field` | field path, typed value, and cited content | field → citation → block → source page |
191
+
192
+ Every entry carries `documentId`, projection and processing-run IDs, stable
193
+ source page, run page, block, normalized geometry, display text, and its source
194
+ kind. Field hits additionally carry field path, typed JSON value, and citation
195
+ ID. A search result is therefore immediately usable as agent evidence; the
196
+ resolver does not have to reconstruct provenance after ranking.
197
+
198
+ `createSearchDocumentsOperation` is the application boundary. It authorizes the
199
+ document before calling the backend and rejects scope leaks, excluded source
200
+ kinds, invalid scores, projection mismatches, excessive result counts, empty
201
+ queries, and unsafe limits. Agent tools and GraphQL resolvers call this named
202
+ operation rather than querying a search table directly:
203
+
204
+ ```ts
205
+ const searchDocuments = createSearchDocumentsOperation({
206
+ authorize: (documentId) => policy.requireDocumentRead(documentId),
207
+ backend: postgresDocumentSearch,
208
+ });
209
+
210
+ const resolvers = {
211
+ Document: {
212
+ search: (document, args, context) =>
213
+ context.documents.search({
214
+ documentId: document.id,
215
+ query: args.query,
216
+ limit: args.limit,
217
+ }),
218
+ },
219
+ };
220
+ ```
221
+
222
+ The included in-memory backend exists only to test this contract. A PostgreSQL
223
+ backend can use full-text search and ranking over `searchText`, normally with a
224
+ GIN index for a frequently searched text vector. PostgreSQL documents GIN as
225
+ the preferred text-search index type.
226
+ [PostgreSQL full-text search](https://www.postgresql.org/docs/current/textsearch.html)
227
+ and [text-search index types](https://www.postgresql.org/docs/current/textsearch-indexes.html).
228
+
229
+ Use `pg_trgm` separately when issuer names, identifiers, or misspellings need
230
+ similarity matching. It supports indexed similarity plus `LIKE` and `ILIKE`, but
231
+ its threshold and ranking behavior remain backend policy—not fields agents may
232
+ mutate through Ablo.
233
+ [PostgreSQL `pg_trgm`](https://www.postgresql.org/docs/current/pgtrgm.html).
234
+
235
+ Projection IDs include the processing run, optional extraction run, and
236
+ projection version. Build new rows, publish the new projection as ready, then
237
+ supersede the old projection. Persisting entries and publishing readiness should
238
+ be one application-owned atomic commit so readers never observe a partial
239
+ index. The original blocks, fields, and citations remain unchanged and can
240
+ rebuild search after tokenizer, language, or ranking policy changes.
241
+
242
+ `planSearchProjectionPublication` makes retention explicit. It only replaces a
243
+ ready projection with a different ready version over the same evidence. When an
244
+ annotation, issue, or review event still references any entry in the current
245
+ version, the plan retains that complete immutable snapshot rather than pruning
246
+ unreferenced siblings and leaving a historically incomplete projection. With no
247
+ references, the complete superseded snapshot becomes removable. The hard
248
+ foreign keys from review records to search entries remain the database
249
+ backstop; the publication operation owns the normal lifecycle policy.
250
+
251
+ ## Guarded annotations and review
252
+
253
+ Annotations and review issues target a retained, versioned
254
+ `documentSearchEntry`. The
255
+ annotation body records what a human or agent said; the target retains the
256
+ processing run, page, block, citation, geometry, and evidence identity that the
257
+ statement refers to. This follows the W3C Web Annotation model's useful body / target
258
+ distinction without claiming full protocol conformance.
259
+ [W3C Web Annotation Data Model](https://www.w3.org/TR/annotation-model/).
260
+
261
+ Enter through `src/documents/review/index.ts`. A UI, agent tool, or GraphQL
262
+ resolver calls the named `addAnnotation`, `requestReview`, or `signOff`
263
+ operation. It does not expose generic status updates as the business API.
264
+ Authorization, valid transitions, attribution, idempotency, evidence checks,
265
+ and atomic writes therefore have one owner.
266
+
267
+ The Ablo adapter is deliberately thin:
268
+
269
+ ```ts
270
+ const ctx = await context({
271
+ ablo,
272
+ data: {
273
+ issue: ablo.documentReviewIssues.get({ id: issueId }),
274
+ target: ablo.documentSearchEntries.get({ id: searchEntryId }),
275
+ },
276
+ });
277
+
278
+ await ablo.commits.create({
279
+ operations: [issueUpdate, signedOffEventCreate],
280
+ reads: ctx.reads,
281
+ claim: issueClaim,
282
+ idempotencyKey,
283
+ });
284
+ ```
285
+
286
+ The coordination responsibilities are deliberately separate:
287
+
288
+ ```text
289
+ claim
290
+ -> reserves the target for one participant while the lease is valid
291
+ -> commit-time fencing rejects a stale or lost holder
292
+
293
+ context().reads
294
+ -> rejects the commit when a captured premise changed
295
+
296
+ atomic commit
297
+ -> persists the complete Ablo state transition together or not at all
298
+
299
+ idempotency key
300
+ -> deduplicates an identical commit within its documented scope
301
+ ```
302
+
303
+ `requestReview` captures the target and optional annotation together, validates
304
+ that both name the same recorded evidence pointer, then atomically creates the issue
305
+ and its `requested` event. `signOff` first claims the issue row, then reads the
306
+ issue and target after the claim is granted, and atomically updates the status
307
+ with a `signed_off` event. If either captured row changes before the commit, the
308
+ batch rejects as stale.
309
+
310
+ The claim is a temporary lease, not workflow storage. A valid claim reserves
311
+ the target for one participant. Foreign commits are rejected while it is held,
312
+ and a stale or lost holder is fenced at commit time. Workers that must exclude
313
+ one another require distinct participant credentials: clients sharing one
314
+ credential are the same participant and do not exclude each other.
315
+ [Ablo coordination](../coordination.md) documents the participant and transport
316
+ semantics. Lease-based systems must expect work to overlap after pauses or
317
+ expiry and protect correctness at the final write.
318
+ [AWS lease guidance](https://aws.amazon.com/builders-library/leader-election-in-distributed-systems/).
319
+
320
+ Durable issue status survives lease expiry and worker failure. Review
321
+ operations create new events and do not modify existing events, but that
322
+ application behavior alone does not make the underlying table append-only.
323
+ Enforce unavailable update/delete operations with database permissions,
324
+ triggers, or equivalent policy before making a storage-level append-only claim.
325
+ A crash before the atomic sign-off leaves the issue open; another authorized
326
+ participant can claim and retry it.
327
+ [Microsoft event-sourcing guidance](https://learn.microsoft.com/en-us/azure/architecture/patterns/event-sourcing)
328
+ describes the stronger immutable event-store pattern and its operational
329
+ requirements.
330
+
331
+ Blocks and citations are canonical evidence. Search entries are versioned
332
+ evidence pointers or grounded projection snapshots. This reference design
333
+ requires the application to retain referenced projection versions and entries
334
+ and prohibit deletion while referenced. An alternative is to store an immutable
335
+ target snapshot on the annotation together with canonical block and citation
336
+ references. Publish a new projection version instead of rewriting a referenced
337
+ entry.
338
+
339
+ An idempotency key deduplicates replay of the identical Ablo commit within the
340
+ server's documented organization, participant, and retention scope. Ordinary
341
+ recorded results are retained for 24 hours. It does not make provider calls,
342
+ emails, storage writes, or other external effects idempotent; those systems need
343
+ their own keys and replay policy.
344
+ [Ablo idempotency](../idempotency.md) is the governing replay contract.
345
+
346
+ Both humans and agents can author annotations. Application authorization still
347
+ decides who may request review and who may sign off; `actorKind` is attribution,
348
+ not permission.
349
+
350
+ ## Page claim granularity
351
+
352
+ Use the row-backed object form for an existing processing-run page row:
353
+
354
+ ```ts
355
+ const claim = await ablo.documentProcessingRunPages.claim({
356
+ id: processingRunPageId,
357
+ contention: { mode: 'skip' },
358
+ });
359
+ ```
360
+
361
+ Claims on the same run-page row conflict. Claims on different run-page IDs can
362
+ coexist, so pages of one PDF may process concurrently without mixing separate
363
+ processing runs.
364
+
365
+ That exclusion is participant-scoped and lease-scoped. Separate workers use
366
+ separate participant credentials. A worker must treat claim loss as loss of
367
+ authority to commit; the commit-time claim fence and captured-read checks are
368
+ the correctness boundary when work overlaps after expiry or a network pause.
369
+
370
+ `parent: true` controls ownership, access inheritance, and sync routing. It does
371
+ not create hierarchical claim conflicts across different model rows. A claim
372
+ on `documents/document_1` does not automatically conflict with a run-page
373
+ claim. Whole-document exclusion is not exercised by this example. That application
374
+ protocol requires an authoritative run manifest, stable acquisition order,
375
+ release after partial acquisition, and a guarded manifest/version check.
376
+
377
+ ## Operation contract
378
+
379
+ The extractor sees evidence, not storage infrastructure:
380
+
381
+ ```ts
382
+ interface DocumentSource {
383
+ documentId: string;
384
+ sourceVersion: string;
385
+ }
386
+
387
+ interface ExistingDocumentService {
388
+ readSource(documentId: string): Promise<DocumentSource | undefined>;
389
+ readCompleted(runId: string): Promise<ProcessedDocument | undefined>;
390
+ process(input, prepare): Promise<ProcessDocumentResult>;
391
+ commitPrepared(input, source, prepared): Promise<ProcessDocumentResult>;
392
+ }
393
+ ```
394
+
395
+ The old path calls `process`, preserving extraction inside the existing
396
+ critical section. The coordinated path claims `documentId`, checks for a
397
+ completed replay, reads the current source version, extracts once, and calls
398
+ `commitPrepared`.
399
+
400
+ The coordination adapter uses `model.claim(documentId, options)`, the
401
+ identifier-only overload. It does not read a model row. A model namespace must
402
+ already be registered in the connected Ablo schema; declaring an empty model in
403
+ this example is not permission for an integration agent to provision or replace
404
+ an inherited production schema. Do not replace it with
405
+ `model.claim({ id: documentId })`: the object form reads a fresh model row and
406
+ would incorrectly couple this operation to an Ablo document snapshot.
407
+
408
+ `commitPrepared` must re-read the authoritative document and compare its source
409
+ version with the version supplied to the extractor. If they differ, reject the
410
+ result. Do not attach output produced from old bytes to a newer document.
411
+
412
+ ## Provenance
413
+
414
+ Every accepted result records:
415
+
416
+ ```text
417
+ documentId
418
+ sourceVersion
419
+ runId
420
+ extractorVersion
421
+ ```
422
+
423
+ The example's identity helpers are deterministic only for the same canonical
424
+ inputs, encoding rules, namespace, and version. Treat those rules as persisted
425
+ schema: changing normalization or identity format requires an explicit new
426
+ version and migration. Deterministic IDs support replay; they do not extend
427
+ Ablo's idempotency retention window or deduplicate external effects.
428
+
429
+ Source pages, run pages, blocks, fields, search projections, and review issues
430
+ should refer back to this evidence. Do not encode storage URLs or file bytes in provenance;
431
+ the existing application resolves its own document reference.
432
+
433
+ ## Verification
434
+
435
+ ```bash
436
+ cd examples/existing-document-pipeline
437
+ npm test
438
+ npm run typecheck
439
+ ```
440
+
441
+ The executable checks exercise:
442
+
443
+ - result parity between the existing and coordinated paths;
444
+ - expensive extraction outside the retained database critical section;
445
+ - one extraction when two workers contend;
446
+ - stale-result rejection after a source-version change;
447
+ - claim release and retry after extractor failure;
448
+ - exact run and extractor provenance;
449
+ - idempotent replay without another extraction;
450
+ - stable source-page and run-owned output-page relations;
451
+ - complete page manifests, including pages without blocks;
452
+ - noncontiguous source-page manifests;
453
+ - run-specific block identity and normalized confidence validation;
454
+ - nested JSON-pointer field identity and one-to-many citations;
455
+ - complete field → citation → block → source-page traceability;
456
+ - unified issuer, field, amount, and block search results;
457
+ - authorization and backend scope validation;
458
+ - versioned projection rebuilds without evidence mutation;
459
+ - guarded human and agent annotations over retained projection targets;
460
+ - atomic review-request issue and event creation;
461
+ - claim-serialized sign-off with atomic status and event persistence;
462
+ - stale target and stale issue rejection with safe retry;
463
+ - exclusion on the same page; and
464
+ - concurrent work on different pages.
465
+
466
+ These checks demonstrate the application contract under deterministic
467
+ repositories and claim fixtures. Production guarantees additionally depend on
468
+ the connected database transaction boundary, authorization policy, lease store,
469
+ participant credentials, projection-retention policy, and deployment
470
+ configuration.
471
+
472
+ Run [Hosted coordination conformance](./coordination-conformance.md) separately
473
+ for real delegated identities, heartbeat, exclusion, and process-death expiry.
474
+ Keeping that proof independent prevents this domain example from pushing a
475
+ document schema merely to retest the lease primitive.
476
+
477
+ ## Non-goals
478
+
479
+ This pattern does not own uploads, signed URLs, downloads, object storage,
480
+ malware scanning, retention, or deletion. Those systems pre-exist the
481
+ coordination operation and remain unchanged.
482
+
483
+ It does not define external durable workflow orchestration, missing-item
484
+ detection, or leadsheet generation. Those can consume the durable review and
485
+ grounded evidence state without moving their execution lifecycle into a claim.
486
+
487
+ None of the coordination mechanisms in this example automatically protects
488
+ external side effects.
@@ -65,7 +65,7 @@ secret key — then pass it to the provider via `client`.
65
65
  // web/app/providers.tsx
66
66
  'use client';
67
67
 
68
- import Ablo from '@abloatai/ablo';
68
+ import { Ablo } from '@abloatai/ablo/react';
69
69
  import { AbloProvider } from '@abloatai/ablo/react';
70
70
  import { schema } from '@/ablo/schema';
71
71
 
@@ -248,21 +248,19 @@ Ablo -> realtime fanout and receipt
248
248
  The app does not need a flag-day rewrite. Move one model at a time.
249
249
 
250
250
  ```ts
251
- const snap = ablo.snapshot({ weatherReports: reportId });
251
+ const report = await ablo.weatherReports.read({ id: reportId });
252
+ if (!report) throw new Error('report not found');
252
253
 
253
254
  await ablo.weatherReports.update({
254
255
  id: reportId,
255
256
  data: { status: 'ready' },
256
- readAt: snap.stamp,
257
- onStale: 'reject',
257
+ reads: [report],
258
258
  });
259
259
  ```
260
260
 
261
- Use `readAt` and `onStale: 'reject'` for actions that depend on state the user
262
- or agent already saw. If two people both click "mark ready" on a report one of
263
- them already finished, `onStale: 'reject'` makes the second write fail instead
264
- of silently clobbering — `readAt: snap.stamp` is the version the user actually
265
- saw, and the write is rejected if the row changed underneath them.
261
+ Use `read` and pass its exact result in `reads` for actions that depend on state
262
+ the user or agent already saw. If the row changed underneath that decision, the
263
+ write rejects instead of silently clobbering the newer value.
266
264
 
267
265
  ## 5. Report Direct Database Writes
268
266
 
@@ -285,14 +283,13 @@ and timestamp. If the change originated from an Ablo commit, include the same
285
283
  Agents use the same model API as the UI:
286
284
 
287
285
  ```ts
288
- const report = await ablo.weatherReports.get({ id: reportId });
289
- const snap = ablo.snapshot({ weatherReports: reportId });
286
+ const report = await ablo.weatherReports.read({ id: reportId });
287
+ if (!report) throw new Error('report not found');
290
288
 
291
289
  await ablo.weatherReports.update({
292
290
  id: reportId,
293
291
  data: { status: 'ready' },
294
- readAt: snap.stamp,
295
- onStale: 'reject',
292
+ reads: [report],
296
293
  });
297
294
  ```
298
295
 
@@ -148,7 +148,7 @@ isn't torn down on every render.
148
148
  // app/providers.tsx
149
149
  'use client';
150
150
 
151
- import Ablo from '@abloatai/ablo';
151
+ import { Ablo } from '@abloatai/ablo/react';
152
152
  import { AbloProvider } from '@abloatai/ablo/react';
153
153
  import { schema } from '@/lib/ablo.schema';
154
154
 
@@ -188,7 +188,7 @@ export default async function RecordPage({
188
188
  }: { params: Promise<{ id: string }> }) {
189
189
  const { id } = await params;
190
190
  await ablo.ready();
191
- const record = await ablo.records.get({ id });
191
+ const record = await ablo.records.read({ id });
192
192
  if (!record) return null;
193
193
 
194
194
  return <RecordEditor record={record} />;
@@ -64,9 +64,14 @@ import { schema } from './schema';
64
64
 
65
65
  const server = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
66
66
 
67
- export async function mintProjectAgentSession(workspaceId: string, agentId: string) {
67
+ export async function mintProjectAgentSession(
68
+ workspaceId: string,
69
+ agentId: string,
70
+ requestingUserId: string,
71
+ ) {
68
72
  const { token } = await server.sessions.create({
69
73
  agent: { id: agentId },
74
+ onBehalfOf: { user: { id: requestingUserId } },
70
75
  can: { records: ['read', 'update'] }, // operation allowlist for this run
71
76
  syncGroups: [syncGroup('workspace', workspaceId)], // narrowed to just this workspace
72
77
  });
@@ -74,6 +79,18 @@ export async function mintProjectAgentSession(workspaceId: string, agentId: stri
74
79
  }
75
80
  ```
76
81
 
82
+ If work crosses a queue, persist `requestingUserId` on the job before enqueueing
83
+ it and read that stored value when minting the worker's agent session. Request
84
+ context and in-memory enqueue arguments disappear across retries and process
85
+ boundaries; a durable job without its delegator can only produce agent-only or
86
+ system-only attribution.
87
+
88
+ In a collaborative deployment, do not silently fall back to direct database
89
+ writes when agent-session minting is unavailable. Those writes can still be
90
+ observed through WAL, but they have no trusted correlation and are therefore
91
+ recorded as `system`. Either fail the job for retry or make uncoordinated writes
92
+ an explicit deployment mode.
93
+
77
94
  ```tsx
78
95
  // client — the browser client carries only the scoped token.
79
96
  import Ablo from '@abloatai/ablo';
@@ -48,7 +48,7 @@ export async function completeTask(recordId: string, workerId: string) {
48
48
  const ablo = await clientForWorker(workerId);
49
49
  await ablo.ready();
50
50
 
51
- const record = await ablo.records.get({ id: recordId });
51
+ const record = await ablo.records.read({ id: recordId });
52
52
  if (!record) return { status: 'not_found' };
53
53
 
54
54
  const acquired = await ablo.records.claim({
@@ -76,7 +76,7 @@ export async function completeTask(recordId: string, workerId: string) {
76
76
  }
77
77
  ```
78
78
 
79
- `get({ id })` is an async server read — it hits the server and returns the
79
+ `read({ id })` is an async server read — it hits the server and returns the
80
80
  row (or `undefined`, which the early `not_found` guard handles). The update runs
81
81
  while the claim is held; awaiting it resolves only once your database has
82
82
  confirmed the row landed.