@abloatai/ablo 0.57.0 → 0.59.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.
- package/AGENTS.md +12 -5
- package/CHANGELOG.md +269 -13
- package/README.md +6 -5
- package/dist/ai-sdk.d.ts +1 -1
- package/dist/ai-sdk.d.ts.map +1 -1
- package/dist/context/evidence.d.ts +6 -8
- package/dist/context/evidence.d.ts.map +1 -1
- package/dist/context/evidence.js +6 -20
- package/dist/context/evidence.js.map +1 -1
- package/dist/context/index.d.ts +23 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +26 -0
- package/dist/context/index.js.map +1 -0
- package/dist/context/onChange.d.ts +9 -0
- package/dist/context/onChange.d.ts.map +1 -0
- package/dist/context/onChange.js +37 -0
- package/dist/context/onChange.js.map +1 -0
- package/dist/source-conformance.d.ts +1 -1
- package/dist/source-conformance.d.ts.map +1 -1
- package/dist/source-conformance.js +1 -1
- package/dist/source-conformance.js.map +1 -1
- package/dist/source-drizzle.d.ts +1 -1
- package/dist/source-drizzle.d.ts.map +1 -1
- package/dist/source-drizzle.js +1 -1
- package/dist/source-drizzle.js.map +1 -1
- package/dist/source-kysely.d.ts +1 -1
- package/dist/source-kysely.d.ts.map +1 -1
- package/dist/source-kysely.js +1 -1
- package/dist/source-kysely.js.map +1 -1
- package/dist/source-next.d.ts +1 -1
- package/dist/source-next.d.ts.map +1 -1
- package/dist/source-next.js +1 -1
- package/dist/source-next.js.map +1 -1
- package/docs/agents.md +38 -15
- package/docs/api-keys.md +6 -6
- package/docs/api.md +107 -34
- package/docs/basic-usage.md +84 -0
- package/docs/branch-development.md +23 -4
- package/docs/cli.md +16 -9
- package/docs/client-behavior.md +27 -29
- package/docs/comparison.md +63 -0
- package/docs/concurrency-convention.md +92 -75
- package/docs/context.md +76 -31
- package/docs/coordinate-existing-work.md +104 -0
- package/docs/coordination.md +93 -38
- package/docs/data-sources.md +12 -6
- package/docs/debugging.md +1 -1
- package/docs/deployment.md +19 -1
- package/docs/examples/agent-human.md +6 -18
- package/docs/examples/coordination-conformance.md +69 -0
- package/docs/examples/evidence-backed-document-pipeline.md +488 -0
- package/docs/examples/existing-python-backend.md +10 -13
- package/docs/examples/nextjs.md +2 -2
- package/docs/examples/scoped-agent.md +18 -1
- package/docs/examples/server-agent.md +2 -2
- package/docs/faq.md +75 -0
- package/docs/groups.md +19 -139
- package/docs/guarantees.md +8 -8
- package/docs/idempotency.md +3 -0
- package/docs/identity.md +2 -1
- package/docs/implement.md +61 -0
- package/docs/implementation-index.md +20 -0
- package/docs/index.md +59 -173
- package/docs/installation.md +77 -0
- package/docs/instrumentation.md +52 -0
- package/docs/integration-guide.md +20 -19
- package/docs/integrations/sandbox-runtime.md +157 -0
- package/docs/integrations.md +9 -0
- package/docs/migration.md +12 -7
- package/docs/operating-on-your-database.md +7 -0
- package/docs/options.md +172 -0
- package/docs/quickstart.md +25 -14
- package/docs/react.md +9 -9
- package/docs/schema-contract.md +14 -13
- package/docs/security.md +64 -0
- package/docs/sessions.md +1 -1
- package/examples/README.md +8 -2
- package/examples/agent-turn.ts +1 -1
- package/examples/expensive-agent-turn.ts +1 -1
- package/examples/stale-context-agent-turn.ts +106 -0
- package/llms.txt +23 -12
- package/package.json +7 -6
- package/dist/context/sources.d.ts +0 -21
- package/dist/context/sources.d.ts.map +0 -1
- package/dist/context/sources.js +0 -36
- package/dist/context/sources.js.map +0 -1
- package/dist/context.d.ts +0 -22
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js +0 -33
- package/dist/context.js.map +0 -1
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
# Build an evidence-backed document pipeline
|
|
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
|
+
> [coordinate existing work guide](../coordinate-existing-work.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
|
|
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
|
-
|
|
257
|
-
onStale: 'reject',
|
|
257
|
+
reads: [report],
|
|
258
258
|
});
|
|
259
259
|
```
|
|
260
260
|
|
|
261
|
-
Use `
|
|
262
|
-
or agent already saw. If
|
|
263
|
-
|
|
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.
|
|
289
|
-
|
|
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
|
-
|
|
295
|
-
onStale: 'reject',
|
|
292
|
+
reads: [report],
|
|
296
293
|
});
|
|
297
294
|
```
|
|
298
295
|
|
package/docs/examples/nextjs.md
CHANGED
|
@@ -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.
|
|
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(
|
|
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.
|
|
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
|
-
`
|
|
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.
|