@abloatai/ablo 0.56.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.
- package/AGENTS.md +10 -4
- package/CHANGELOG.md +428 -10
- package/LICENSE +1 -1
- package/NOTICE +3 -3
- package/README.md +2 -1
- 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/agent-integration-decision-guide.md +123 -0
- package/docs/agents.md +74 -13
- package/docs/api-keys.md +6 -6
- package/docs/api.md +117 -43
- package/docs/branch-development.md +23 -4
- package/docs/cli.md +16 -9
- package/docs/client-behavior.md +21 -15
- package/docs/concurrency-convention.md +67 -77
- package/docs/context.md +56 -31
- package/docs/coordination.md +115 -36
- package/docs/customer-organizations.md +49 -31
- package/docs/data-sources.md +12 -6
- package/docs/debugging.md +1 -1
- package/docs/examples/agent-human.md +6 -18
- package/docs/examples/coordination-conformance.md +69 -0
- package/docs/examples/existing-document-pipeline.md +488 -0
- package/docs/examples/existing-python-backend.md +10 -13
- package/docs/examples/nextjs.md +49 -6
- package/docs/examples/scoped-agent.md +18 -1
- package/docs/examples/server-agent.md +2 -2
- package/docs/groups.md +19 -139
- package/docs/guarantees.md +5 -6
- package/docs/identity.md +2 -1
- package/docs/index.md +5 -0
- package/docs/integration-guide.md +46 -19
- package/docs/integrations/sandbox-runtime.md +148 -0
- package/docs/integrations.md +9 -0
- package/docs/operating-on-your-database.md +7 -0
- package/docs/quickstart.md +19 -13
- package/docs/react.md +9 -9
- package/docs/schema-contract.md +14 -13
- package/docs/session-settings.md +9 -0
- package/docs/sessions.md +1 -1
- package/examples/README.md +2 -2
- package/examples/agent-turn.ts +1 -1
- package/examples/data-source/customer-server.ts +12 -5
- package/examples/expensive-agent-turn.ts +1 -1
- package/llms.txt +72 -10
- package/package.json +6 -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 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/context/index.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAa,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAEzD,OAAO,EAAyB,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAE5E,YAAY,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAC9C,YAAY,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAI5E,MAAM,WAAW,cAAc,CAAC,KAAK,SAAS,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC7E,kFAAkF;IAClF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,wEAAwE;IACxE,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAC;CACtB;AAED,MAAM,WAAW,aAAa,CAAC,KAAK,SAAS,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5E,QAAQ,CAAC,IAAI,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC;IAClC;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,CAAC;IACvC,qEAAqE;IACrE,QAAQ,CAAC,QAAQ,EAAE,eAAe,CAAC;CACpC;AAED,wBAAsB,OAAO,CAAC,KAAK,CAAC,KAAK,SAAS,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACjF,OAAO,EAAE,cAAc,CAAC,KAAK,CAAC,GAC7B,OAAO,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAgB/B"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Assemble the values selected for one action and retain the exact Ablo reads
|
|
3
|
+
* they contain. Each read keeps its own `readAt`; context does not collapse
|
|
4
|
+
* those independent premises into another watermark.
|
|
5
|
+
*/
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { awaitDeep } from './await.js';
|
|
8
|
+
import { bindContextEvidence } from './evidence.js';
|
|
9
|
+
import { createContextOnChange } from './onChange.js';
|
|
10
|
+
const contextDataSchema = z.record(z.string(), z.unknown());
|
|
11
|
+
export async function context(options) {
|
|
12
|
+
const evidenceBinding = bindContextEvidence(options.ablo);
|
|
13
|
+
const data = await awaitDeep(options.data);
|
|
14
|
+
const parsed = contextDataSchema.safeParse(data);
|
|
15
|
+
if (!parsed.success) {
|
|
16
|
+
throw new TypeError('context() requires `data` to be an object.', { cause: parsed.error });
|
|
17
|
+
}
|
|
18
|
+
const evidence = evidenceBinding.collect(data);
|
|
19
|
+
const dependencies = evidence.map((item) => item.entry);
|
|
20
|
+
return {
|
|
21
|
+
data: data,
|
|
22
|
+
reads: evidence.map((item) => item.row),
|
|
23
|
+
onChange: createContextOnChange(dependencies, evidenceBinding.onChange),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/context/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,OAAO,EAAE,SAAS,EAAoB,MAAM,YAAY,CAAC;AACzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,qBAAqB,EAAwB,MAAM,eAAe,CAAC;AAK5E,MAAM,iBAAiB,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;AAoB5D,MAAM,CAAC,KAAK,UAAU,OAAO,CAC3B,OAA8B;IAE9B,MAAM,eAAe,GAAG,mBAAmB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1D,MAAM,IAAI,GAAG,MAAM,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3C,MAAM,MAAM,GAAG,iBAAiB,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;IACjD,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;QACpB,MAAM,IAAI,SAAS,CAAC,4CAA4C,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;IAC7F,CAAC;IAED,MAAM,QAAQ,GAAG,eAAe,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,MAAM,YAAY,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAExD,OAAO;QACL,IAAI,EAAE,IAA0B;QAChC,KAAK,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,GAAkB,CAAC;QACtD,QAAQ,EAAE,qBAAqB,CAAC,YAAY,EAAE,eAAe,CAAC,QAAQ,CAAC;KACxE,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ReadDependency } from '@abloatai/transaction/coordination';
|
|
2
|
+
import type { AbloStaleContextError } from '@abloatai/transaction';
|
|
3
|
+
export type ContextChangeListener = (error: AbloStaleContextError) => void;
|
|
4
|
+
export type ContextOnChange = (listener: ContextChangeListener) => () => void;
|
|
5
|
+
type StartOnChange = (reads: readonly ReadDependency[], listener: ContextChangeListener) => () => void;
|
|
6
|
+
/** Share one transport subscription for every listener on one context. */
|
|
7
|
+
export declare function createContextOnChange(reads: readonly ReadDependency[], start: StartOnChange | undefined): ContextOnChange;
|
|
8
|
+
export {};
|
|
9
|
+
//# sourceMappingURL=onChange.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"onChange.d.ts","sourceRoot":"","sources":["../../src/context/onChange.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oCAAoC,CAAC;AACzE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAEnE,MAAM,MAAM,qBAAqB,GAAG,CAAC,KAAK,EAAE,qBAAqB,KAAK,IAAI,CAAC;AAC3E,MAAM,MAAM,eAAe,GAAG,CAAC,QAAQ,EAAE,qBAAqB,KAAK,MAAM,IAAI,CAAC;AAE9E,KAAK,aAAa,GAAG,CACnB,KAAK,EAAE,SAAS,cAAc,EAAE,EAChC,QAAQ,EAAE,qBAAqB,KAC5B,MAAM,IAAI,CAAC;AAEhB,0EAA0E;AAC1E,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,SAAS,cAAc,EAAE,EAChC,KAAK,EAAE,aAAa,GAAG,SAAS,GAC/B,eAAe,CAoCjB"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/** Share one transport subscription for every listener on one context. */
|
|
2
|
+
export function createContextOnChange(reads, start) {
|
|
3
|
+
const listeners = new Set();
|
|
4
|
+
let stop;
|
|
5
|
+
let stale;
|
|
6
|
+
const changed = (error) => {
|
|
7
|
+
if (stale)
|
|
8
|
+
return;
|
|
9
|
+
stale = error;
|
|
10
|
+
stop?.();
|
|
11
|
+
stop = undefined;
|
|
12
|
+
for (const listener of [...listeners])
|
|
13
|
+
listener(error);
|
|
14
|
+
};
|
|
15
|
+
return (listener) => {
|
|
16
|
+
if (stale) {
|
|
17
|
+
listener(stale);
|
|
18
|
+
return () => undefined;
|
|
19
|
+
}
|
|
20
|
+
listeners.add(listener);
|
|
21
|
+
if (reads.length > 0 && !stop) {
|
|
22
|
+
if (!start) {
|
|
23
|
+
listeners.delete(listener);
|
|
24
|
+
throw new TypeError('This Ablo client does not support context().onChange.');
|
|
25
|
+
}
|
|
26
|
+
stop = start(reads, changed);
|
|
27
|
+
}
|
|
28
|
+
return () => {
|
|
29
|
+
listeners.delete(listener);
|
|
30
|
+
if (listeners.size === 0) {
|
|
31
|
+
stop?.();
|
|
32
|
+
stop = undefined;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=onChange.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"onChange.js","sourceRoot":"","sources":["../../src/context/onChange.ts"],"names":[],"mappings":"AAWA,0EAA0E;AAC1E,MAAM,UAAU,qBAAqB,CACnC,KAAgC,EAChC,KAAgC;IAEhC,MAAM,SAAS,GAAG,IAAI,GAAG,EAAyB,CAAC;IACnD,IAAI,IAA8B,CAAC;IACnC,IAAI,KAAwC,CAAC;IAE7C,MAAM,OAAO,GAAG,CAAC,KAA4B,EAAQ,EAAE;QACrD,IAAI,KAAK;YAAE,OAAO;QAClB,KAAK,GAAG,KAAK,CAAC;QACd,IAAI,EAAE,EAAE,CAAC;QACT,IAAI,GAAG,SAAS,CAAC;QACjB,KAAK,MAAM,QAAQ,IAAI,CAAC,GAAG,SAAS,CAAC;YAAE,QAAQ,CAAC,KAAK,CAAC,CAAC;IACzD,CAAC,CAAC;IAEF,OAAO,CAAC,QAAQ,EAAE,EAAE;QAClB,IAAI,KAAK,EAAE,CAAC;YACV,QAAQ,CAAC,KAAK,CAAC,CAAC;YAChB,OAAO,GAAG,EAAE,CAAC,SAAS,CAAC;QACzB,CAAC;QAED,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACxB,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;YAC9B,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;gBAC3B,MAAM,IAAI,SAAS,CAAC,uDAAuD,CAAC,CAAC;YAC/E,CAAC;YACD,IAAI,GAAG,KAAK,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QAC/B,CAAC;QAED,OAAO,GAAG,EAAE;YACV,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;YAC3B,IAAI,SAAS,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;gBACzB,IAAI,EAAE,EAAE,CAAC;gBACT,IAAI,GAAG,SAAS,CAAC;YACnB,CAAC;QACH,CAAC,CAAC;IACJ,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from '@abloatai/transaction/source/
|
|
1
|
+
export * from '@abloatai/transaction/source/adapters';
|
|
2
2
|
//# sourceMappingURL=source-conformance.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source-conformance.d.ts","sourceRoot":"","sources":["../src/source-conformance.ts"],"names":[],"mappings":"AAAA,cAAc,
|
|
1
|
+
{"version":3,"file":"source-conformance.d.ts","sourceRoot":"","sources":["../src/source-conformance.ts"],"names":[],"mappings":"AAAA,cAAc,uCAAuC,CAAC"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from '@abloatai/transaction/source/
|
|
1
|
+
export * from '@abloatai/transaction/source/adapters';
|
|
2
2
|
//# sourceMappingURL=source-conformance.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source-conformance.js","sourceRoot":"","sources":["../src/source-conformance.ts"],"names":[],"mappings":"AAAA,cAAc,
|
|
1
|
+
{"version":3,"file":"source-conformance.js","sourceRoot":"","sources":["../src/source-conformance.ts"],"names":[],"mappings":"AAAA,cAAc,uCAAuC,CAAC"}
|
package/dist/source-drizzle.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from '@abloatai/transaction/source/drizzle';
|
|
1
|
+
export * from '@abloatai/transaction/source/adapters/drizzle';
|
|
2
2
|
//# sourceMappingURL=source-drizzle.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source-drizzle.d.ts","sourceRoot":"","sources":["../src/source-drizzle.ts"],"names":[],"mappings":"AAAA,cAAc,
|
|
1
|
+
{"version":3,"file":"source-drizzle.d.ts","sourceRoot":"","sources":["../src/source-drizzle.ts"],"names":[],"mappings":"AAAA,cAAc,+CAA+C,CAAC"}
|
package/dist/source-drizzle.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from '@abloatai/transaction/source/drizzle';
|
|
1
|
+
export * from '@abloatai/transaction/source/adapters/drizzle';
|
|
2
2
|
//# sourceMappingURL=source-drizzle.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source-drizzle.js","sourceRoot":"","sources":["../src/source-drizzle.ts"],"names":[],"mappings":"AAAA,cAAc,
|
|
1
|
+
{"version":3,"file":"source-drizzle.js","sourceRoot":"","sources":["../src/source-drizzle.ts"],"names":[],"mappings":"AAAA,cAAc,+CAA+C,CAAC"}
|
package/dist/source-kysely.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from '@abloatai/transaction/source/kysely';
|
|
1
|
+
export * from '@abloatai/transaction/source/adapters/kysely';
|
|
2
2
|
//# sourceMappingURL=source-kysely.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source-kysely.d.ts","sourceRoot":"","sources":["../src/source-kysely.ts"],"names":[],"mappings":"AAAA,cAAc,
|
|
1
|
+
{"version":3,"file":"source-kysely.d.ts","sourceRoot":"","sources":["../src/source-kysely.ts"],"names":[],"mappings":"AAAA,cAAc,8CAA8C,CAAC"}
|
package/dist/source-kysely.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from '@abloatai/transaction/source/kysely';
|
|
1
|
+
export * from '@abloatai/transaction/source/adapters/kysely';
|
|
2
2
|
//# sourceMappingURL=source-kysely.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source-kysely.js","sourceRoot":"","sources":["../src/source-kysely.ts"],"names":[],"mappings":"AAAA,cAAc,
|
|
1
|
+
{"version":3,"file":"source-kysely.js","sourceRoot":"","sources":["../src/source-kysely.ts"],"names":[],"mappings":"AAAA,cAAc,8CAA8C,CAAC"}
|
package/dist/source-next.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from '@abloatai/transaction/source/
|
|
1
|
+
export * from '@abloatai/transaction/source/endpoint';
|
|
2
2
|
//# sourceMappingURL=source-next.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source-next.d.ts","sourceRoot":"","sources":["../src/source-next.ts"],"names":[],"mappings":"AAAA,cAAc,
|
|
1
|
+
{"version":3,"file":"source-next.d.ts","sourceRoot":"","sources":["../src/source-next.ts"],"names":[],"mappings":"AAAA,cAAc,uCAAuC,CAAC"}
|
package/dist/source-next.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export * from '@abloatai/transaction/source/
|
|
1
|
+
export * from '@abloatai/transaction/source/endpoint';
|
|
2
2
|
//# sourceMappingURL=source-next.js.map
|
package/dist/source-next.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source-next.js","sourceRoot":"","sources":["../src/source-next.ts"],"names":[],"mappings":"AAAA,cAAc,
|
|
1
|
+
{"version":3,"file":"source-next.js","sourceRoot":"","sources":["../src/source-next.ts"],"names":[],"mappings":"AAAA,cAAc,uCAAuC,CAAC"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Agent Integration Decision Guide
|
|
2
|
+
|
|
3
|
+
> Choose one integration route before reading an example. Most existing products should coordinate one existing operation first; they should not copy the document pipeline.
|
|
4
|
+
|
|
5
|
+
## Start with the operation you already own
|
|
6
|
+
|
|
7
|
+
Name one existing application operation, such as `completeTask`, `approveInvoice`,
|
|
8
|
+
or `publishReport`. Keep its authorization, database transaction, constraints,
|
|
9
|
+
and public API in place. Add Ablo at that operation boundary.
|
|
10
|
+
|
|
11
|
+
Use this routing table for the decisions that are easy to conflate:
|
|
12
|
+
|
|
13
|
+
| Question | Choose | When |
|
|
14
|
+
|---|---|---|
|
|
15
|
+
| What is being coordinated? | Identifier-only claim | Work has a stable business identity, but the authoritative row and final write remain in the existing service or Postgres. |
|
|
16
|
+
| | Row-backed claim | The coordinated row is an Ablo schema model and the final write goes through that model resource. |
|
|
17
|
+
| Did the decision depend on previously read rows? | Captured reads | Read the premises with `read(...)`, then pass those returned rows through `reads`. Use this even when the written row is different from a premise row. |
|
|
18
|
+
| Must several Ablo mutations either all land or none land? | Atomic commit | Put the mutations, captured reads, and any typed claim handles in one `commits.create(...)`. Separate mutation calls are independently successful or failed. |
|
|
19
|
+
| Where should the final write happen? | Existing database write | Keep it when the current service owns the transaction, constraints, or rollout switch. Ablo's lease does not join a transaction running in another process. Re-read and validate inside the database transaction. |
|
|
20
|
+
| | Ablo-routed write | Use it for declared schema models after the database connection and server schema are configured. Guard decision-dependent writes with a held claim or captured reads. |
|
|
21
|
+
| Who is participating? | Stateless HTTP client | Agents, jobs, and request/response server handlers. Give each concurrent participant its own scoped credential. |
|
|
22
|
+
| | Reactive WebSocket client | Human-facing applications that need live state, presence, or local reactive reads. This transport is not required for worker coordination. |
|
|
23
|
+
|
|
24
|
+
These choices compose. For example, a stateless worker can take an
|
|
25
|
+
identifier-only claim, perform slow work, and then call an existing Postgres
|
|
26
|
+
operation. Another worker can take a row-backed claim and submit an atomic Ablo
|
|
27
|
+
commit guarded by captured premise rows.
|
|
28
|
+
|
|
29
|
+
## Choose the smallest example
|
|
30
|
+
|
|
31
|
+
### Coordinate existing work
|
|
32
|
+
|
|
33
|
+
Start with
|
|
34
|
+
[`examples/graphql-existing-backend`](../../../examples/graphql-existing-backend/README.md)
|
|
35
|
+
when an application already owns its API, operation, and Postgres write.
|
|
36
|
+
|
|
37
|
+
It demonstrates:
|
|
38
|
+
|
|
39
|
+
- GraphQL delegating to a named application operation;
|
|
40
|
+
- an identifier lease around expensive work;
|
|
41
|
+
- the existing service retaining its authoritative transaction and re-read;
|
|
42
|
+
- an operation-level switch between existing and coordinated paths; and
|
|
43
|
+
- recovery and contract parity without replacing the application's API.
|
|
44
|
+
|
|
45
|
+
Use
|
|
46
|
+
[`examples/coordination-conformance`](../../../examples/coordination-conformance/README.md)
|
|
47
|
+
alongside it to verify real hosted lease behavior independently of the domain.
|
|
48
|
+
|
|
49
|
+
### Build evidence-backed document state
|
|
50
|
+
|
|
51
|
+
Read
|
|
52
|
+
[`examples/existing-document-pipeline`](../../../examples/existing-document-pipeline/README.md)
|
|
53
|
+
only when the feature genuinely needs versioned source evidence, citations,
|
|
54
|
+
guarded review decisions, atomic multi-row review writes, and retained search
|
|
55
|
+
projections.
|
|
56
|
+
|
|
57
|
+
That example is an advanced reference application. Its document ingestion,
|
|
58
|
+
search, review, projection-retention, and append-only event policies are not
|
|
59
|
+
prerequisites for adopting Ablo.
|
|
60
|
+
|
|
61
|
+
## Know which owner makes each promise
|
|
62
|
+
|
|
63
|
+
| Ablo responsibility | Application responsibility |
|
|
64
|
+
|---|---|
|
|
65
|
+
| Participant-scoped claims, lease expiry, wait/skip behavior, heartbeat, release, and commit-time fencing | Choosing the business claim identity and issuing distinct participant credentials |
|
|
66
|
+
| Capturing model-row versions returned by `read(...)` and rejecting a guarded write when those premises are stale | Choosing every row that is a premise of the decision |
|
|
67
|
+
| Atomicity among mutations submitted in one Ablo commit | Database transactions and constraints outside that commit; never presenting separate writes as an atomic batch |
|
|
68
|
+
| Request idempotency within the documented identity, retention, and identical-request rules | Durable workflow idempotency and deduplication of external effects |
|
|
69
|
+
| Synchronizing declared model rows and serving reactive state | Uploads, search semantics, projections, workflow execution, review policy, and external APIs |
|
|
70
|
+
| Credentials, participant attribution, and schema-declared scope enforcement | Existing application authentication and authorization at the operation boundary |
|
|
71
|
+
|
|
72
|
+
Claims coordinate cooperative participants; they are leases, not absolute locks.
|
|
73
|
+
A writer outside the coordinated path can still change Postgres. Database
|
|
74
|
+
constraints and a commit-time guarded re-read remain the final backstop.
|
|
75
|
+
|
|
76
|
+
## Minimum integration contract
|
|
77
|
+
|
|
78
|
+
Write down these answers next to the operation before implementing it:
|
|
79
|
+
|
|
80
|
+
1. **Existing operation:** Which named operation and public API remain stable?
|
|
81
|
+
2. **Claim identity:** Which stable model row or business identifier represents the contested work?
|
|
82
|
+
3. **Participant identity:** Which distinct scoped credential does each concurrent human, agent, or worker use?
|
|
83
|
+
4. **Premises:** Which exact rows does the decision depend on, and which of them must use `read(...)`?
|
|
84
|
+
5. **Atomic boundary:** Which writes must all succeed together? Are they one Ablo commit, one existing database transaction, or deliberately independent?
|
|
85
|
+
6. **Persistence owner:** What remains in Postgres and which writes, if any, are routed through Ablo?
|
|
86
|
+
7. **Failure behavior:** What happens on contention, lease expiry, stale evidence, request retry, partial completion, and an external side-effect failure?
|
|
87
|
+
8. **Proof:** Which local contract test and which hosted or staging test proves each claimed guarantee?
|
|
88
|
+
|
|
89
|
+
If an answer is unknown, keep the existing write path available. Do not broaden
|
|
90
|
+
the schema or copy an advanced example to hide the missing decision.
|
|
91
|
+
|
|
92
|
+
## Guarantee-to-test matrix
|
|
93
|
+
|
|
94
|
+
The examples prove different layers. A local fixture proves application behavior;
|
|
95
|
+
it does not prove hosted infrastructure. Conversely, hosted claim conformance
|
|
96
|
+
does not prove a domain transition or a real Postgres transaction.
|
|
97
|
+
|
|
98
|
+
| Statement | Proof level | Executable evidence |
|
|
99
|
+
|---|---|---|
|
|
100
|
+
| GraphQL keeps the same result while the named operation changes implementation | Local application contract | `examples/graphql-existing-backend/tests/graphql.test.ts` and `tests/pilot.test.ts` — `the GraphQL resolver delegates one typed input to the named operation`; `the operation switch preserves the uncontended GraphQL contract` |
|
|
101
|
+
| Coordination moves expensive work outside the retained database critical section | Local application contract; real DB timing requires staging | `examples/graphql-existing-backend/tests/pilot.test.ts` — `coordination moves expensive work outside the retained critical section`; optional `npm run test:live:postgres` |
|
|
102
|
+
| Contenders do not duplicate expensive work, and failure releases the path | Local application contract | `examples/graphql-existing-backend/tests/pilot.test.ts` — `two coordinated workers pay for expensive work once`; `a failed owner releases coordination so the existing path remains available` |
|
|
103
|
+
| Distinct hosted participants exclude one another, heartbeat, release, and recover after expiry | Hosted infrastructure | `examples/coordination-conformance`: `npm run test:live`; its live runner also executes the exit-without-release expiry probe |
|
|
104
|
+
| A decision based on changed source evidence is rejected | Local application contract using Ablo-shaped guards | `examples/existing-document-pipeline/tests/processDocument.test.ts` — `a source change rejects stale extracted output`; `tests/review.test.ts` — `guarded mutations reject stale evidence and release claims for retry` |
|
|
105
|
+
| Several review records submitted as one commit are atomic; separate calls can partially complete | Local application contract | `examples/existing-document-pipeline/tests/review.test.ts` — `requesting review atomically creates durable issue state and an event`; `independent review writes expose partial completion and retry only the stale target` |
|
|
106
|
+
| Rebuilding search does not remove a complete snapshot referenced by durable review evidence | Local application policy | `examples/existing-document-pipeline/tests/search.test.ts` — `publishing a rebuild retains the complete projection snapshot referenced by review`; `an unreferenced superseded projection becomes removable as one snapshot` |
|
|
107
|
+
| The documented ownership tree, public exports, dependency direction, and lack of cycles match disk | Local structure contract | Each focused example's `tests/structure.test.ts`; the document fixture additionally validates exact inventory and dependency direction from `structure.json` |
|
|
108
|
+
| Authorization, latency, database locking, and external-effect behavior match the production application | Partner staging | Run the operation against the real auth, Postgres schema, workload, and provider sandbox. No repository fixture can establish this claim. |
|
|
109
|
+
|
|
110
|
+
## Safe first adoption
|
|
111
|
+
|
|
112
|
+
For an existing product, the default sequence is:
|
|
113
|
+
|
|
114
|
+
1. Wrap one named operation without changing its public API.
|
|
115
|
+
2. Use an identifier-only claim if the existing database remains authoritative.
|
|
116
|
+
3. Keep the Postgres transaction, lock, validation, and constraints in place.
|
|
117
|
+
4. Verify participant-scoped lease behavior with coordination conformance.
|
|
118
|
+
5. Add captured reads or an atomic Ablo commit only when the operation actually needs them.
|
|
119
|
+
6. Move more persistence through Ablo only after the guarded hosted write path and staging behavior are proven for that operation.
|
|
120
|
+
|
|
121
|
+
Continue with the [Integration Guide](./integration-guide.md) for setup and API
|
|
122
|
+
details, or [Concurrency Convention](./concurrency-convention.md) for the exact
|
|
123
|
+
guarding rules.
|
package/docs/agents.md
CHANGED
|
@@ -34,7 +34,7 @@ const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY, transport: "http"
|
|
|
34
34
|
// `get` resolves to the row, or `undefined` when none matches.
|
|
35
35
|
const open = await ablo.records.list({ where: { status: "todo" } });
|
|
36
36
|
|
|
37
|
-
const record = await ablo.records.
|
|
37
|
+
const record = await ablo.records.read({ id: open[0].id });
|
|
38
38
|
if (!record) throw new Error("record not found");
|
|
39
39
|
|
|
40
40
|
console.log(record.title);
|
|
@@ -42,9 +42,68 @@ await ablo.records.update({ id: record.id, data: { status: "done" } });
|
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
It exposes `get` / `list` / `create` / `update` / `delete`, plus `commits`
|
|
45
|
-
and `claim`. It does **not** expose stateful-only `local` reads or
|
|
46
|
-
subscriptions. Those need a
|
|
47
|
-
are compile errors
|
|
45
|
+
and `claim`. It does **not** expose stateful-only `local` reads or model
|
|
46
|
+
`onChange` subscriptions. Those need a WebSocket, so with `transport: 'http'`
|
|
47
|
+
they are compile errors. `context().onChange` is separate: while its listener
|
|
48
|
+
is active, it holds one HTTP response open until the context changes or the
|
|
49
|
+
listener stops.
|
|
50
|
+
|
|
51
|
+
## Managed scoped agents
|
|
52
|
+
|
|
53
|
+
When this process owns the secret client and also runs the agent, prefer
|
|
54
|
+
`agents.create`. It mints the restricted credential, returns a schema-typed
|
|
55
|
+
client, and renews that credential for a long run. `sessions.create({ agent })`
|
|
56
|
+
is the raw-token path for handing identity to another runtime.
|
|
57
|
+
|
|
58
|
+
Derive identity and groups from the run row or trusted job payload—not from
|
|
59
|
+
model output or an HTTP request body. A serverless handler normally creates and
|
|
60
|
+
disposes one child per invocation:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
const run = await control.runs.read({ id: verifiedRunId });
|
|
64
|
+
if (!run) throw new Error('run not found');
|
|
65
|
+
|
|
66
|
+
const agent = await control.agents.create({
|
|
67
|
+
id: `run:${run.id}`,
|
|
68
|
+
name: 'run-worker',
|
|
69
|
+
can: { records: ['read', 'update'] },
|
|
70
|
+
syncGroups: [`workspace:${run.workspaceId}`],
|
|
71
|
+
});
|
|
72
|
+
try {
|
|
73
|
+
await executeRun(agent, run);
|
|
74
|
+
} finally {
|
|
75
|
+
await agent.dispose();
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Use a stable id only when one logical run is serialized; two concurrent workers
|
|
80
|
+
that share an id appear as the same participant. For independent concurrent
|
|
81
|
+
work, omit `id` and let Ablo create distinct identities.
|
|
82
|
+
|
|
83
|
+
A long-running worker may cache one managed client per stable scope, but the
|
|
84
|
+
cache owns lifecycle: evict idle clients, call `dispose()` on eviction, and
|
|
85
|
+
dispose every client during graceful shutdown. Never cache a client and later
|
|
86
|
+
reuse it for a different workspace or capability set.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
const agents: Record<
|
|
90
|
+
string,
|
|
91
|
+
Awaited<ReturnType<typeof control.agents.create>> | undefined
|
|
92
|
+
> = {};
|
|
93
|
+
|
|
94
|
+
async function agentFor(run: Run) {
|
|
95
|
+
const key = `${run.workspaceId}:${run.workerSlot}`;
|
|
96
|
+
const cached = agents[key];
|
|
97
|
+
if (cached) return cached;
|
|
98
|
+
const created = await control.agents.create({
|
|
99
|
+
id: `worker:${key}`,
|
|
100
|
+
can: { records: ['read', 'update'] },
|
|
101
|
+
syncGroups: [`workspace:${run.workspaceId}`],
|
|
102
|
+
});
|
|
103
|
+
agents[key] = created;
|
|
104
|
+
return created;
|
|
105
|
+
}
|
|
106
|
+
```
|
|
48
107
|
|
|
49
108
|
## AI SDK tools
|
|
50
109
|
|
|
@@ -147,19 +206,21 @@ default caller here, not a bolt-on.
|
|
|
147
206
|
|
|
148
207
|
```text
|
|
149
208
|
something happens ──▶ your agent (HTTP, no socket)
|
|
150
|
-
(a job, a webhook, read context (list/get)
|
|
209
|
+
(a job, a webhook, read context (list/get/read)
|
|
151
210
|
a queue message) claim → work → commit
|
|
152
211
|
done — no held connection
|
|
153
212
|
```
|
|
154
213
|
|
|
155
|
-
|
|
156
|
-
restarts are free, and you scale by adding
|
|
157
|
-
|
|
214
|
+
Without `context().onChange`, an agent holds nothing open and remains a
|
|
215
|
+
**stateless worker**: deploys and restarts are free, and you scale by adding
|
|
216
|
+
workers. Each active context listener is explicit connection capacity and must
|
|
217
|
+
be stopped when its work ends.
|
|
158
218
|
|
|
159
219
|
## What stays on the live (human) plane
|
|
160
220
|
|
|
161
|
-
`onChange`
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
221
|
+
Model `onChange` and `local` reads require a WebSocket and a local store —
|
|
222
|
+
they're for interactive UIs. An HTTP agent normally reacts to an external
|
|
223
|
+
trigger, then reads with `list`/`get`. For costly work, `context().onChange` can
|
|
224
|
+
stop that one run early while the final write still uses `context().reads`.
|
|
225
|
+
See [client behavior](/client-behavior) for the full surface and
|
|
226
|
+
[guarantees](/guarantees) for the coordination semantics.
|
package/docs/api-keys.md
CHANGED
|
@@ -23,9 +23,9 @@ and remap them before each command.
|
|
|
23
23
|
|
|
24
24
|
| Job | Credential | How you get it |
|
|
25
25
|
|---|---|---|
|
|
26
|
-
| Manage a project or its branches | `mk_` | `npx ablo login
|
|
26
|
+
| Manage a project or its branches | `mk_` | `npx ablo login` stores it for the CLI; pick the project in the terminal, or name it with `--project <slug>`. |
|
|
27
27
|
| Develop locally | expiring `sk_` bound to the current branch | `npx ablo dev` writes it as `ABLO_API_KEY` in gitignored `.env.local`. |
|
|
28
|
-
| Prepare a branch once, including CI | expiring `sk_` bound to that branch | `npx ablo dev --no-watch --branch <ref>`; CI supplies `
|
|
28
|
+
| Prepare a branch once, including CI | expiring `sk_` bound to that branch | `npx ablo dev --no-watch --branch <ref>`; headless CI supplies an `mk_` credential through `ABLO_API_KEY`. |
|
|
29
29
|
| Run the production backend | `sk_` bound to the production root | Store it as the deployment's `ABLO_API_KEY`. |
|
|
30
30
|
| Read in a browser | `pk_` | Publishable, read-only key. |
|
|
31
31
|
| Write in a browser as a user | short-lived `ek_` | Your backend exposes `authEndpoint` and mints it. |
|
|
@@ -33,9 +33,9 @@ and remap them before each command.
|
|
|
33
33
|
The everyday loop is therefore:
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
npx ablo login
|
|
37
|
-
npx ablo dev
|
|
38
|
-
npx ablo status
|
|
36
|
+
npx ablo login # once per project: approve in the browser, pick the project
|
|
37
|
+
npx ablo dev # follows Git, mints and wires this branch
|
|
38
|
+
npx ablo status # broad readiness report
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
Application code and agents still read one variable:
|
|
@@ -91,7 +91,7 @@ The credential class lives in the prefix:
|
|
|
91
91
|
|
|
92
92
|
| Prefix | Purpose | Stored where |
|
|
93
93
|
|---|---|---|
|
|
94
|
-
| `mk_` | project and branch management | CLI credential store
|
|
94
|
+
| `mk_` | project and branch management | CLI credential store; `ABLO_API_KEY` only in headless automation |
|
|
95
95
|
| `sk_` | trusted runtime, full branch authority | server-side `ABLO_API_KEY` |
|
|
96
96
|
| `rk_` | restricted runtime or agent | trusted runtime that needs the delegated scope |
|
|
97
97
|
| `pk_` | publishable, browser-safe read access | browser bundle |
|