@abloatai/ablo 0.9.1 → 0.9.2

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 ADDED
@@ -0,0 +1,84 @@
1
+ # AGENTS.md
2
+
3
+ Ablo lets AI agents and humans safely edit the same typed data without clobbering each other. When two of them touch the same row, a "claim" makes one wait for the other instead of overwriting it. This file shows a coding assistant the one safe pattern: read a row, claim it, then write.
4
+
5
+ Claims don't lock. If another writer holds the row, `claim` waits for them and re-reads the fresh row before handing it to you — so two writers serialize instead of clobbering.
6
+
7
+ ## Start here — scaffold with `ablo init`
8
+
9
+ Don't hand-write the integration. Run the CLI; it generates the current-API schema, client, Data Source endpoint, and (for Next.js) the browser provider + session route:
10
+
11
+ - **Scaffold:** `npx ablo init --yes` — flag-driven, never prompts. Override defaults with `--framework <nextjs|vite|remix|vanilla>`, `--auth <apikey|…>`, `--storage <datasource|managed>`, `--no-agent`, `--no-pull`, `--no-install`, `--no-login`. (Plain `ablo init` needs a TTY and will **HANG** in an agent/CI run — always pass `--yes`.)
12
+ - **Auth:** set `ABLO_API_KEY` in the environment. Do **NOT** run `ablo login` — it opens a browser device flow and blocks an agent.
13
+ - **Provision your DB (Data Source mode):** `npx ablo migrate` creates the tables for your Ablo models plus the adapter's bookkeeping tables (`ablo_outbox`, `ablo_idempotency`). It does **not** touch your other tables — keep your own migrations (drizzle-kit / prisma migrate) for auth and anything not in the Ablo schema.
14
+ - **Adopt an existing DB schema:** `npx ablo pull prisma [path]` / `pull drizzle <module>` (lossless) or `pull` (live DB, lossy). Writes `ablo/schema.ts`.
15
+ - **Push your schema — REQUIRED before any write works.** The server keeps its OWN copy of the schema. After you create or edit `ablo/schema.ts`, run `npx ablo push` (one-shot) — or `npx ablo dev --no-watch`. **Skip this and every write to a new or changed model fails with `server_execute_unknown_model`.** (Plain `ablo dev` watches forever — never run it bare in an agent.)
16
+ - **Other long-running:** `npx ablo logs --no-follow` (default tails forever). `npx ablo mode test|live` ALWAYS pass the argument. `status`, `push`, `pull`, `check`, `generate` are one-shot — safe as-is.
17
+
18
+ The generated `ablo/data-source.ts` is the whole Data Source endpoint and needs no hand-editing: `dataSourceNext({ schema, apiKey, adapter: prismaDataSource(prisma, schema) })` (or `drizzleDataSource(db, schema)`). The adapter owns commit / idempotency / outbox.
19
+
20
+ ## Rule
21
+
22
+ Edit the generated files; teach this API only:
23
+
24
+ ```ts
25
+ const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
26
+ ```
27
+
28
+ The schema is the integration contract — it drives typed model clients, React selectors, server and agent writes, the Data Source shape, and schema push. Ablo owns only the models you declare; your auth and other non-synced tables stay in your own ORM schema, side by side in the same database. Don't create a parallel string-keyed write path for rows that belong to a schema model.
29
+
30
+ Every model verb takes ONE options object. The common loop:
31
+
32
+ 1. **Read** the row — `await ablo.<model>.retrieve({ id })` (async; from the server) or `await ablo.<model>.list({ where })` for many. In React render, read synchronously with `useAblo((a) => a.<model>.get(id))`.
33
+ 2. **See who's active** (optional) — `ablo.<model>.claim.state({ id })` (synchronous; never blocks).
34
+ 3. **Claim** the row before changing it — `await using claim = await ablo.<model>.claim({ id, action?, ttl? })`. If someone else holds it, this waits for them, then gives you the fresh row on `claim.data`. The claim auto-releases when it goes out of scope (`await using`).
35
+ 4. **Write** — `await ablo.<model>.update({ id: claim.data.id, data })`. Because you hold the claim, the write is rejected if the row changed underneath you.
36
+
37
+ Keep coding assistants on this schema-backed path.
38
+
39
+ ## Minimal example
40
+
41
+ ```ts
42
+ import Ablo from '@abloatai/ablo';
43
+ import { defineSchema, model, z } from '@abloatai/ablo/schema';
44
+
45
+ const schema = defineSchema({
46
+ weatherReports: model({
47
+ location: z.string(),
48
+ status: z.enum(['pending', 'ready']),
49
+ forecast: z.string().optional(),
50
+ }),
51
+ });
52
+
53
+ const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
54
+
55
+ const report = await ablo.weatherReports.retrieve({ id: 'report_stockholm' });
56
+ if (!report) throw new Error('Report not found');
57
+
58
+ // If someone else holds the row, claim waits for them and re-reads the fresh
59
+ // row before resolving. Auto-released at the end of this scope (`await using`).
60
+ await using claim = await ablo.weatherReports.claim({
61
+ id: 'report_stockholm',
62
+ action: 'forecasting',
63
+ ttl: '2m',
64
+ });
65
+ const claimed = claim.data;
66
+
67
+ // Because we hold the claim, update is rejected if the row changed underneath us.
68
+ await ablo.weatherReports.update({
69
+ id: claimed.id,
70
+ data: { status: 'ready', forecast: await getForecast(claimed.location) },
71
+ });
72
+ ```
73
+
74
+ ## Coordination surface
75
+
76
+ Claims live on a callable namespace beside `create` / `update` / `retrieve`. Every member takes an options object:
77
+
78
+ - `await using claim = await ablo.<model>.claim({ id })` — acquire the row (waits if held); read it via `claim.data`; auto-releases on scope exit (or call `claim.release()`).
79
+ - `ablo.<model>.claim.state({ id })` — who is currently working on the row (synchronous; never blocks).
80
+ - `ablo.<model>.claim.queue({ id })` — who is waiting behind the current holder.
81
+ - `ablo.<model>.claim.release({ id })` — release a claim early.
82
+ - `ablo.<model>.claim.reorder({ id, order })` — reorder the waiting queue.
83
+
84
+ Most users declare a schema and write through `ablo.<model>.update({ id, data })`.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,39 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.9.2
4
+
5
+ ### Patch Changes
6
+
7
+ - Developer-onboarding overhaul so an LLM or a person gets a working integration on the first try.
8
+ - **`ablo init` scaffolds a project that builds and is current-API.** The Next.js scaffold now ships `app/providers.tsx` + an `app/api/ablo-session` route, uses `useAblo` (the removed `withSync` is gone), object-param verbs, and never bundles your `sk_` key into the browser. The webhook receiver moved off the `[...all]` catch-all.
9
+ - **Agent docs are accurate and ship.** `AGENTS.md`, `llms.txt`, and `llms-full.txt` are on the 0.9.x API (object-param `create`/`update`/`delete`/`retrieve`, disposable `await using claim`, `AbloProvider client` prop), lead with `ablo init`, and `AGENTS.md` now ships in the package.
10
+ - **`ablo push` is self-documenting.** Writing to a model the server hasn't seen now fails with an error that tells you to run `ablo push` (the `server_execute_unknown_model` / `unknown_model` messages), instead of a cryptic "unknown model."
11
+ - **`intents` is deprecated in favor of `claim`** everywhere the docs and the MCP scaffold/prompts teach or generate coordination; the public `ablo.intents` accessor is marked `@internal`.
12
+ - Docs say Node 24+, and the `drizzle-orm` peer floor is `>=0.44`.
13
+
14
+ - a88747a: Remove the `turn` primitive and the agent-work `tasks` resource from the client surface — the SDK is now purely `ablo.<model>` + `claim`.
15
+
16
+ **Breaking**
17
+ - `engine.beginTurn()`, the `Turn` handle interface, and the `Ablo.Turn` type are removed. `AbloApi.beginTurn` and the HTTP client's `beginTurn` are gone too.
18
+ - `CommitCreateOptions.causedByTaskId` is removed. (Lineage is no longer stamped from the client.)
19
+ - The engine no longer exposes a `protocol` accessor or a public `tasks` work-unit resource. `ablo.tasks` is, and always was, the schema `tasks` model proxy.
20
+ - The **`agent().run()` helper and the low-level agent/task type family are removed**: `AbloApi.agent(id, options)` and `AbloApi.tasks` (the `TaskResource`), plus the exported types `Agent`, `AgentOptions`, `AgentRunOptions`, `AgentRunResult`/`Done`/`Failed`/`Cancelled`, `AgentRunStatus`, `AgentRunContext`, `AgentModelClient`, `AgentModelReadOptions`, `AgentModelMutationOptions`, `AgentIntentOptions`, `AgentIntentInput`, `Task`, `TaskResource`, `TaskCreateOptions`, `TaskCloseOptions`, `TaskCloseResult` (and the `Ablo.*` namespace aliases for all of them). The `Ablo.Auth.Agent` principal constructor and the schema-backed `tasks` model are unaffected.
21
+
22
+ **Why**
23
+
24
+ `turn`/`agent_tasks` was a second coordination-and-attribution mechanism living alongside `claim`. It is redundant on the client:
25
+ - `claim` already serializes writers **and** carries the causal link — its `intent` id rides on every guarded write.
26
+ - The server stamps `actor` / `onBehalfOf` / `capabilityId` onto each delta from the auth context.
27
+ - Per-run token/cost is recorded in Langfuse, not the `agent_tasks` table.
28
+
29
+ So the only thing the client lost is the audit pane's "show everything this exact prompt produced" filter, which keyed off `caused_by_task_id`; new writes leave that column null.
30
+
31
+ **Migration**
32
+
33
+ Agents stop opening/closing tasks — just issue `ablo.<model>` writes (schema-backed) or `ablo.commits.create(...)` (schema-less) under a `claim`. Replace `Ablo({ apiKey }).agent(id, opts).run(prompt, handler)` with: mint a scoped credential via `sessions.create({ agent })`, then `claim` the row and `update` / `commits.create`.
34
+
35
+ The **server** `agent_tasks` table, the `caused_by_task_id` delta column, the `/api/sync/commit` wire field, and the `agent_actions_log` compliance hash-chain remain in place but **dormant** (client writes leave the field null) — they are load-bearing for the tamper-evident audit chain and historical-row audit JOINs, so they are intentionally NOT dropped. The dead `/v1/tasks` + `/api/agent/turn` route handlers ARE removed (zero live callers).
36
+
3
37
  ## 0.9.1
4
38
 
5
39
  ### Patch Changes
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm](https://img.shields.io/npm/v/@abloatai/ablo.svg)](https://www.npmjs.com/package/@abloatai/ablo)
4
4
  [![license](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](./LICENSE)
5
5
  [![types](https://img.shields.io/badge/types-included-blue.svg)](#)
6
- [![runtime](https://img.shields.io/badge/node-%E2%89%A522-brightgreen.svg)](#keys--runtime)
6
+ [![runtime](https://img.shields.io/badge/node-%E2%89%A524-brightgreen.svg)](#keys--runtime)
7
7
 
8
8
  **Let people and AI agents work on the same data without overwriting each other.**
9
9
 
@@ -46,7 +46,7 @@ anywhere people and agents change shared state and everyone has to see it live.
46
46
  npm install @abloatai/ablo
47
47
  ```
48
48
 
49
- **Keys & runtime.** Ablo needs Node 22+ and TypeScript 5+. Grab an `sk_test_*`
49
+ **Keys & runtime.** Ablo needs Node 24+ and TypeScript 5+. Grab an `sk_test_*`
50
50
  key for a sandbox
51
51
  (`export ABLO_API_KEY=sk_test_...`); keep keys in trusted server runtimes only.
52
52
  In the browser, `<AbloProvider>` authenticates with the signed-in user's
@@ -218,12 +218,16 @@ provider and read with hooks, from `@abloatai/ablo/react`. Wrap your tree once;
218
218
  everything inside is live.
219
219
 
220
220
  ```tsx
221
+ import Ablo from '@abloatai/ablo';
221
222
  import { AbloProvider, useAblo } from '@abloatai/ablo/react';
222
223
  import { schema } from './ablo/schema';
223
224
 
225
+ // Build the client once — it authenticates via your session route, no key in the browser.
226
+ const ablo = Ablo({ schema, authEndpoint: '/api/ablo-session' });
227
+
224
228
  function App() {
225
229
  return (
226
- <AbloProvider schema={schema}>
230
+ <AbloProvider client={ablo}>
227
231
  <Report id="report_stockholm" />
228
232
  </AbloProvider>
229
233
  );
@@ -250,8 +254,9 @@ method as the server example above.
250
254
  `<AbloProvider>` owns the connection — no API key in the browser. That's the
251
255
  whole loop: read with `useAblo(selector)`, write with `ablo.<model>`, and every
252
256
  other client (human or agent) on that row sees it in real time. See
253
- [React](./docs/react.md) for the full `<AbloProvider>` prop surface (`userId`,
254
- `teamIds`, `syncGroups`, `fallback`, `bootstrapMode`) and status hooks.
257
+ [React](./docs/react.md) for the `<AbloProvider>` prop surface (`client`,
258
+ `userId`, `fallback`, `onError`) — schema, scope, and team membership live on the
259
+ `Ablo({ … })` client you pass it — plus status hooks.
255
260
 
256
261
  ## Identity & Sync Groups
257
262
 
@@ -270,7 +275,10 @@ to sync-group strings.
270
275
  `userId` / `teamIds` come from your auth, resolved server-side:
271
276
 
272
277
  ```tsx
273
- <AbloProvider schema={schema} userId={user.id} teamIds={user.teamIds}>
278
+ // team membership is asserted server-side when the session route mints the token.
279
+ const ablo = Ablo({ schema, authEndpoint: '/api/ablo-session' });
280
+
281
+ <AbloProvider client={ablo} userId={user.id}>
274
282
  <App />
275
283
  </AbloProvider>
276
284
  ```
@@ -421,8 +421,12 @@ export class BaseSyncedStore {
421
421
  * emit here, so undo is naturally local-only (you can't undo a teammate).
422
422
  */
423
423
  subscribeLocalMutations(handler) {
424
- return this.syncClient.subscribe('transaction:created', (data) => {
425
- const tx = data;
424
+ // Tap the TransactionQueue directly via `onLocalTransaction`. The previous
425
+ // `syncClient.subscribe('transaction:created', …)` route registered the
426
+ // handler on SyncClient's OWN emitter, which never fires that event (only
427
+ // the queue's emitter does) — so undo recorded nothing. See
428
+ // `SyncClient.onLocalTransaction` for the full rationale.
429
+ return this.syncClient.onLocalTransaction((tx) => {
426
430
  if (!tx || !tx.type || !tx.modelName || !tx.modelId)
427
431
  return;
428
432
  handler({
@@ -383,6 +383,18 @@ export declare class SyncClient extends EventEmitter {
383
383
  error: Error;
384
384
  permanent?: boolean;
385
385
  }) => void): () => void;
386
+ /**
387
+ * Subscribe to LOCAL transaction creation with the full {@link Transaction}
388
+ * payload (`type`, `modelName`, `modelId`, `data`, `previousData`). This is
389
+ * the feed `BaseSyncedStore.subscribeLocalMutations` taps for undo recording.
390
+ *
391
+ * MUST subscribe to the TransactionQueue's emitter directly — that is the
392
+ * ONLY emitter that fires `transaction:created`. SyncClient's own emitter
393
+ * (reached via `subscribe()`) never re-broadcasts it, so routing undo through
394
+ * `subscribe('transaction:created')` silently records nothing. Mirrors
395
+ * `onMutationFailure`, which taps the queue for the same reason.
396
+ */
397
+ onLocalTransaction(listener: (tx: import('./transactions/TransactionQueue.js').Transaction) => void): () => void;
386
398
  /**
387
399
  * Wait for the latest in-flight transaction for (modelName, modelId)
388
400
  * to be confirmed by the server, or reject if it's rolled back.
@@ -1298,6 +1298,21 @@ export class SyncClient extends EventEmitter {
1298
1298
  this.transactionQueue.on('transaction:failed', listener);
1299
1299
  return () => this.transactionQueue.off('transaction:failed', listener);
1300
1300
  }
1301
+ /**
1302
+ * Subscribe to LOCAL transaction creation with the full {@link Transaction}
1303
+ * payload (`type`, `modelName`, `modelId`, `data`, `previousData`). This is
1304
+ * the feed `BaseSyncedStore.subscribeLocalMutations` taps for undo recording.
1305
+ *
1306
+ * MUST subscribe to the TransactionQueue's emitter directly — that is the
1307
+ * ONLY emitter that fires `transaction:created`. SyncClient's own emitter
1308
+ * (reached via `subscribe()`) never re-broadcasts it, so routing undo through
1309
+ * `subscribe('transaction:created')` silently records nothing. Mirrors
1310
+ * `onMutationFailure`, which taps the queue for the same reason.
1311
+ */
1312
+ onLocalTransaction(listener) {
1313
+ this.transactionQueue.on('transaction:created', listener);
1314
+ return () => this.transactionQueue.off('transaction:created', listener);
1315
+ }
1301
1316
  /**
1302
1317
  * Wait for the latest in-flight transaction for (modelName, modelId)
1303
1318
  * to be confirmed by the server, or reject if it's rolled back.
@@ -122,7 +122,7 @@
122
122
  // const ctx: Agent.Context = { perception };
123
123
  // const s: Agent.SessionOptions = { ... };
124
124
  //
125
- // Everything else (Activity, Claim, Turn, Peer, ActiveIntent, ...)
125
+ // Everything else (Activity, Claim, Peer, ActiveIntent, ...)
126
126
  // lives on the `Ablo.*` namespace via
127
127
  // `import type { Ablo } from '@abloatai/ablo'`.
128
128
  export { Agent } from './Agent.js';
@@ -4,7 +4,7 @@
4
4
  * Use this build for serverless functions, scripts, and backends that want
5
5
  * model reads/writes and commits over HTTP without the realtime sync runtime.
6
6
  */
7
- export { createProtocolClient, createProtocolClient as Ablo, type AbloApi, type AbloApiClientOptions, type AbloApiIntents, type Agent, type AgentIntentInput, type AgentIntentOptions, type AgentOptions, type AgentModelClient, type AgentModelReadOptions, type AgentModelMutationOptions, type AgentRunContext, type AgentRunDone, type AgentRunFailed, type AgentRunCancelled, type AgentRunOptions, type AgentRunResult, type AgentRunStatus, type Capability, type CapabilityCreateOptions, type CapabilityParticipantKind, type CapabilityRecord, type CapabilityResource, type CapabilityRevocation, type CapabilityScope, type Task, type TaskCloseOptions, type TaskCloseResult, type TaskCreateOptions, type TaskResource, } from '../client/ApiClient.js';
7
+ export { createProtocolClient, createProtocolClient as Ablo, type AbloApi, type AbloApiClientOptions, type AbloApiIntents, type Capability, type CapabilityCreateOptions, type CapabilityParticipantKind, type CapabilityRecord, type CapabilityResource, type CapabilityRevocation, type CapabilityScope, } from '../client/ApiClient.js';
8
8
  export type { CommitCreateOptions, CommitOperationInput, CommitReceipt, CommitWait, IntentCreateOptions, IntentHandle, IntentWaitOptions, ClaimedOptions, IfClaimedPolicy, ModelClient, ModelClaim, ModelMutationOptions, ModelReadOptions, ModelRead, ModelTarget, } from '../client/Ablo.js';
9
9
  import { createProtocolClient } from '../client/ApiClient.js';
10
10
  export default createProtocolClient;
@@ -28,22 +28,6 @@ import type { IntentStream, IntentWaitOptions, PresenceStream, Snapshot } from '
28
28
  import type { ParticipantManager } from '../sync/participants.js';
29
29
  import type { ActiveIntent, Duration, Intent, TargetRange } from '../types/streams.js';
30
30
  import { type AbloApi, type AbloApiClientOptions, type AbloApiIntents } from './ApiClient.js';
31
- /**
32
- * Handle returned by `engine.beginTurn()`. While alive, every commit
33
- * automatically carries this turn's id on the wire. Call `close(stats?)`
34
- * when the turn finishes, or `dispose()` to abandon without recording
35
- * usage. Idempotent.
36
- */
37
- export interface Turn {
38
- readonly turnId: string;
39
- close(stats?: {
40
- readonly costInputTokens?: number;
41
- readonly costOutputTokens?: number;
42
- readonly costComputeMs?: number;
43
- }): Promise<void>;
44
- dispose(): void;
45
- [Symbol.asyncDispose](): Promise<void>;
46
- }
47
31
  /**
48
32
  * Async function that resolves an apiKey at request time. Use for
49
33
  * credential rotation — rotate from a vault, refresh from session
@@ -809,10 +793,13 @@ export type Ablo<S extends SchemaRecord> = {
809
793
  */
810
794
  readonly presence: PresenceStream;
811
795
  /**
812
- * Cooperative-mutex layer over presence announce "I'm about to do
813
- * X on Y" so peers can yield before colliding. Server enforces the
814
- * mutex; rejected announcements surface via `intents.onRejected(...)`.
815
- * Same socket as entity sync, no second connection.
796
+ * @internal the public coordination API is `ablo.<model>.claim`. This
797
+ * accessor is the internal stream `claim` is built on; it is NOT part of the
798
+ * supported public surface and will be moved off the public type (it currently
799
+ * stays only because internal SDK modules are still typed against it).
800
+ *
801
+ * Cooperative-mutex layer over presence — announce "I'm about to do X on Y" so
802
+ * peers can yield before colliding. Same socket as entity sync.
816
803
  */
817
804
  readonly intents: IntentResource;
818
805
  /**
@@ -861,18 +848,6 @@ export type Ablo<S extends SchemaRecord> = {
861
848
  snapshot<ModelName extends keyof S & string>(entities: {
862
849
  readonly [M in ModelName]: string | readonly string[];
863
850
  }): Snapshot<Schema<S>, ModelName>;
864
- /**
865
- * Open a turn — every commit issued while the returned handle is
866
- * alive carries `caused_by_task_id` on the wire so the server
867
- * stamps it onto each delta. Powers `agent_tasks` audit trails.
868
- * Server: `POST /api/agent/turn` with the capability bearer.
869
- */
870
- beginTurn(options: {
871
- readonly prompt: string;
872
- readonly parentTaskId?: string;
873
- readonly surface?: string;
874
- readonly metadata?: Record<string, unknown>;
875
- }): Promise<Turn>;
876
851
  /**
877
852
  * The internal BaseSyncedStore. Implements SyncStoreContract — pass to
878
853
  * SyncContext.Provider so the SDK's useModel/useModels/useMutations hooks
@@ -973,17 +948,6 @@ export declare namespace Ablo {
973
948
  type Options<S extends SchemaRecord = SchemaRecord> = AbloOptions<S>;
974
949
  type Api = AbloApi;
975
950
  type ApiIntents = AbloApiIntents;
976
- type Agent = import('./ApiClient.js').Agent;
977
- type AgentOptions = import('./ApiClient.js').AgentOptions;
978
- type AgentRunOptions = import('./ApiClient.js').AgentRunOptions;
979
- type AgentRunStatus = import('./ApiClient.js').AgentRunStatus;
980
- type AgentRunResult<T> = import('./ApiClient.js').AgentRunResult<T>;
981
- type AgentRunContext = import('./ApiClient.js').AgentRunContext;
982
- type AgentModelClient<T = Record<string, unknown>> = import('./ApiClient.js').AgentModelClient<T>;
983
- type AgentModelReadOptions = import('./ApiClient.js').AgentModelReadOptions;
984
- type AgentModelMutationOptions = import('./ApiClient.js').AgentModelMutationOptions;
985
- type AgentIntentOptions = import('./ApiClient.js').AgentIntentOptions;
986
- type AgentIntentInput = import('./ApiClient.js').AgentIntentInput;
987
951
  type Capability = import('./ApiClient.js').Capability;
988
952
  type CapabilityCreateOptions = import('./ApiClient.js').CapabilityCreateOptions;
989
953
  type CapabilityRecord = import('./ApiClient.js').CapabilityRecord;
@@ -991,11 +955,6 @@ export declare namespace Ablo {
991
955
  type CapabilityRevocation = import('./ApiClient.js').CapabilityRevocation;
992
956
  type CapabilityRotateOptions = import('./ApiClient.js').CapabilityRotateOptions;
993
957
  type RotatedCapability = import('./ApiClient.js').RotatedCapability;
994
- type Task = import('./ApiClient.js').Task;
995
- type TaskCreateOptions = import('./ApiClient.js').TaskCreateOptions;
996
- type TaskCloseOptions = import('./ApiClient.js').TaskCloseOptions;
997
- type TaskCloseResult = import('./ApiClient.js').TaskCloseResult;
998
- type TaskResource = import('./ApiClient.js').TaskResource;
999
958
  type IfClaimedPolicy = import('./Ablo.js').IfClaimedPolicy;
1000
959
  type ClaimedOptions = import('./Ablo.js').ClaimedOptions;
1001
960
  type EntityRef = _Streams.EntityRef;
@@ -1011,7 +970,6 @@ export declare namespace Ablo {
1011
970
  type IntentRejection = _Streams.IntentRejection;
1012
971
  type IntentLost = _Streams.IntentLost;
1013
972
  type Snapshot<TSchema extends _SchemaTypes.Schema = _SchemaTypes.Schema, K extends keyof TSchema['models'] = keyof TSchema['models']> = _Streams.Snapshot<TSchema, K>;
1014
- type Turn = import('./Ablo.js').Turn;
1015
973
  namespace Auth {
1016
974
  type Principal = _Streams.Principal;
1017
975
  type Session = _Streams.SessionRef;
@@ -19,7 +19,7 @@
19
19
  * await sync.reports.delete({ id: reportId });
20
20
  */
21
21
  import { z } from 'zod';
22
- import { AbloClaimedError, AbloError, AbloAuthenticationError, AbloConnectionError, AbloValidationError, translateHttpError, hasWireCode, toAbloError } from '../errors.js';
22
+ import { AbloClaimedError, AbloError, AbloAuthenticationError, AbloConnectionError, AbloValidationError, translateHttpError, toAbloError } from '../errors.js';
23
23
  import { LoadStrategy, PropertyType } from '../types/index.js';
24
24
  import { initSyncEngine } from '../context.js';
25
25
  import { noopObservability, browserOnlineStatus, defaultSessionErrorDetector, noopAnalytics, } from '../SyncEngineContext.js';
@@ -812,13 +812,6 @@ export function Ablo(options) {
812
812
  // becomes null, so the first Ablo's commits start throwing
813
813
  // `ws_not_ready` forever (terminal AgentJob writes hang on retry).
814
814
  syncClient.getTransactionQueue().setMutationExecutor(executor);
815
- // Active turn id, set by `beginTurn(...)`, cleared on close. While
816
- // set, every batch commit attaches `causedByTaskId` so server
817
- // delta rows get stamped with it. Single-turn-at-a-time per Ablo
818
- // — opening a second turn overwrites the active id without closing
819
- // the prior. Callers who need parallel turns construct multiple
820
- // Ablo instances, matching the SyncAgent semantics.
821
- let activeTurnId = null;
822
815
  // Presence + intent streams — built eagerly so `engine.presence`
823
816
  // and `engine.intents` return the same reference for the engine's
824
817
  // lifetime. The transport doesn't exist yet (BaseSyncedStore.initialize
@@ -1363,9 +1356,7 @@ export function Ablo(options) {
1363
1356
  // SyncClient we already hold from createInternalComponents —
1364
1357
  // no need to leak an accessor through BaseSyncedStore.
1365
1358
  const queue = syncClient.getTransactionQueue();
1366
- queue.enqueueCommit(clientTxId, operations, {
1367
- causedByTaskId: activeTurnId,
1368
- });
1359
+ queue.enqueueCommit(clientTxId, operations);
1369
1360
  if (wait === 'queued') {
1370
1361
  return { id: clientTxId, status: 'queued' };
1371
1362
  }
@@ -1638,99 +1629,6 @@ export function Ablo(options) {
1638
1629
  entities,
1639
1630
  });
1640
1631
  },
1641
- // ── Turn handles ────────────────────────────────────────────────
1642
- //
1643
- // Open a turn — every commit issued while the returned handle is
1644
- // alive carries `caused_by_task_id` on the wire so the server
1645
- // stamps it onto each delta. The product surface this powers:
1646
- // `agent_tasks` audit trails ("which AI prompt produced this
1647
- // mutation"), parent/child turn chains, cost accounting per turn.
1648
- //
1649
- // POST /api/agent/turn (capability bearer) → returns turnId.
1650
- // POST /api/agent/turn/:id/close (capability bearer) → records
1651
- // final cost stats. Idempotent.
1652
- async beginTurn(beginOptions) {
1653
- const baseUrl = url.replace(/\/+$/, '');
1654
- const turnUrl = `${baseUrl.replace(/^ws/, 'http')}/api/agent/turn`;
1655
- const headers = authCredentials.withAuthHeaders({ 'Content-Type': 'application/json' });
1656
- const res = await fetch(turnUrl, {
1657
- method: 'POST',
1658
- headers,
1659
- body: JSON.stringify({
1660
- prompt: beginOptions.prompt,
1661
- parentTaskId: beginOptions.parentTaskId,
1662
- surface: beginOptions.surface,
1663
- metadata: beginOptions.metadata,
1664
- }),
1665
- });
1666
- if (!res.ok) {
1667
- const text = await res.text().catch(() => '');
1668
- let parsed = text;
1669
- if (text) {
1670
- try {
1671
- parsed = JSON.parse(text);
1672
- }
1673
- catch {
1674
- /* keep raw text */
1675
- }
1676
- }
1677
- // Preserve the server's structured envelope (code/message/doc_url) when
1678
- // present; fall back to turn_open_failed for a bare/non-Ablo body.
1679
- throw hasWireCode(parsed)
1680
- ? translateHttpError(res.status, parsed, res.headers.get('x-request-id') ?? undefined)
1681
- : new AbloError(`beginTurn failed: ${res.status} ${text}`, {
1682
- code: 'turn_open_failed',
1683
- httpStatus: res.status,
1684
- });
1685
- }
1686
- const json = (await res.json());
1687
- const turnId = json.turnId;
1688
- activeTurnId = turnId;
1689
- let closed = false;
1690
- const close = async (stats) => {
1691
- if (closed)
1692
- return;
1693
- closed = true;
1694
- if (activeTurnId === turnId)
1695
- activeTurnId = null;
1696
- const closeUrl = `${turnUrl}/${encodeURIComponent(turnId)}/close`;
1697
- const closeRes = await fetch(closeUrl, {
1698
- method: 'POST',
1699
- headers,
1700
- body: JSON.stringify({
1701
- costInputTokens: stats?.costInputTokens ?? 0,
1702
- costOutputTokens: stats?.costOutputTokens ?? 0,
1703
- costComputeMs: stats?.costComputeMs ?? 0,
1704
- }),
1705
- });
1706
- if (!closeRes.ok) {
1707
- const text = await closeRes.text().catch(() => '');
1708
- let parsed = text;
1709
- if (text) {
1710
- try {
1711
- parsed = JSON.parse(text);
1712
- }
1713
- catch {
1714
- /* keep raw text */
1715
- }
1716
- }
1717
- throw hasWireCode(parsed)
1718
- ? translateHttpError(closeRes.status, parsed, closeRes.headers.get('x-request-id') ?? undefined)
1719
- : new AbloError(`closeTurn failed: ${closeRes.status} ${text}`, {
1720
- code: 'turn_close_failed',
1721
- httpStatus: closeRes.status,
1722
- });
1723
- }
1724
- };
1725
- const dispose = () => {
1726
- if (closed)
1727
- return;
1728
- closed = true;
1729
- if (activeTurnId === turnId)
1730
- activeTurnId = null;
1731
- };
1732
- return { turnId, close, dispose, [Symbol.asyncDispose]: () => close() };
1733
- },
1734
1632
  };
1735
1633
  return engine;
1736
1634
  }
@@ -5,7 +5,7 @@
5
5
  * IndexedDB, no WebSocket. It maps the public Model / Claim / Commit
6
6
  * nouns directly to HTTP routes on sync-server.
7
7
  */
8
- import type { AbloOptions, CommitReceipt, CommitResource, IntentCreateOptions, IntentHandle, IntentWaitOptions, ModelClient, ModelClaim, ModelMutationOptions, ModelReadOptions, ModelRead, ModelTarget, Turn } from './Ablo.js';
8
+ import type { AbloOptions, CommitResource, IntentCreateOptions, IntentHandle, IntentWaitOptions, ModelClient, ModelClaim, ModelTarget } from './Ablo.js';
9
9
  import type { Duration } from '../utils/duration.js';
10
10
  export type AbloApiClientOptions = Omit<AbloOptions, 'schema'> & {
11
11
  readonly schema?: null | undefined;
@@ -115,127 +115,15 @@ export interface CapabilityResource {
115
115
  */
116
116
  mint(options: CapabilityCreateOptions): Promise<Capability>;
117
117
  }
118
- export interface TaskCreateOptions {
119
- readonly prompt: string;
120
- readonly parentTaskId?: string;
121
- readonly surface?: string;
122
- readonly metadata?: Record<string, unknown>;
123
- }
124
- export interface TaskCloseOptions {
125
- readonly costInputTokens?: number;
126
- readonly costOutputTokens?: number;
127
- readonly costComputeMs?: number;
128
- }
129
- export interface Task {
130
- readonly id: string;
131
- readonly turnId: string;
132
- readonly promptHash?: string;
133
- readonly openedAt?: string;
134
- close(stats?: TaskCloseOptions): Promise<TaskCloseResult>;
135
- }
136
- export interface TaskCloseResult {
137
- readonly id: string;
138
- readonly turnId: string;
139
- readonly closed: boolean;
140
- readonly alreadyClosed?: boolean;
141
- readonly endedAt?: string;
142
- }
143
- export interface TaskResource {
144
- create(options: TaskCreateOptions): Promise<Task>;
145
- close(id: string, stats?: TaskCloseOptions): Promise<TaskCloseResult>;
146
- /**
147
- * Alias for `create`. Kept for the agent-run vocabulary; `create` is
148
- * the canonical SDK verb.
149
- */
150
- open(options: TaskCreateOptions): Promise<Task>;
151
- }
152
- export interface AgentOptions {
153
- readonly can: readonly string[];
154
- readonly syncGroups?: readonly string[];
155
- readonly label?: string;
156
- readonly userMeta?: Record<string, unknown>;
157
- /**
158
- * Internal lease for the run capability. Most callers should omit it.
159
- * The SDK revokes the capability when `run` finishes; the lease exists
160
- * to clean up crashed or abandoned runs.
161
- */
162
- readonly lease?: Duration;
163
- readonly leaseSeconds?: number;
164
- }
165
- export interface AgentRunOptions extends TaskCreateOptions {
166
- readonly signal?: AbortSignal;
167
- readonly costInputTokens?: number;
168
- readonly costOutputTokens?: number;
169
- readonly costComputeMs?: number;
170
- }
171
- export type AgentRunStatus = 'done' | 'failed' | 'cancelled';
172
- export interface AgentRunDone<T> {
173
- readonly status: 'done';
174
- readonly task: Task;
175
- readonly value: T;
176
- }
177
- export interface AgentRunFailed {
178
- readonly status: 'failed';
179
- readonly task?: Task;
180
- readonly error: unknown;
181
- }
182
- export interface AgentRunCancelled {
183
- readonly status: 'cancelled';
184
- readonly task?: Task;
185
- readonly error?: unknown;
186
- }
187
- export type AgentRunResult<T> = AgentRunDone<T> | AgentRunFailed | AgentRunCancelled;
188
- export interface AgentIntentOptions {
189
- readonly action: string;
190
- readonly field?: string;
191
- readonly ttl?: Duration;
192
- readonly target?: Partial<ModelTarget>;
193
- }
194
- export type AgentIntentInput = string | AgentIntentOptions;
195
- export interface AgentModelReadOptions extends ModelReadOptions {
196
- }
197
- export interface AgentModelMutationOptions extends Omit<ModelMutationOptions, 'intent'> {
198
- readonly intent?: AgentIntentInput | {
199
- readonly id: string;
200
- } | null;
201
- }
202
- export interface AgentModelClient<T = Record<string, unknown>> {
203
- retrieve(params: AgentModelReadOptions & {
204
- readonly id: string;
205
- }): Promise<ModelRead<T>>;
206
- create(params: AgentModelMutationOptions & {
207
- readonly data: Record<string, unknown>;
208
- readonly id?: string | null;
209
- }): Promise<CommitReceipt>;
210
- update(params: AgentModelMutationOptions & {
211
- readonly id: string;
212
- readonly data: Record<string, unknown>;
213
- }): Promise<CommitReceipt>;
214
- delete(params: AgentModelMutationOptions & {
215
- readonly id: string;
216
- }): Promise<CommitReceipt>;
217
- }
218
- export interface AgentRunContext {
219
- readonly task: Task;
220
- readonly ablo: AbloApi;
221
- model<T = Record<string, unknown>>(name: string): AgentModelClient<T>;
222
- }
223
- export interface Agent {
224
- readonly id: string;
225
- run<T>(options: AgentRunOptions, handler: (context: AgentRunContext) => Promise<T> | T): Promise<AgentRunResult<T>>;
226
- }
227
118
  export interface AbloApi {
228
119
  ready(): Promise<void>;
229
120
  waitForFlush(): Promise<void>;
230
121
  dispose(): Promise<void>;
231
122
  purge(): Promise<void>;
232
123
  readonly capabilities: CapabilityResource;
233
- readonly tasks: TaskResource;
234
124
  readonly intents: AbloApiIntents;
235
125
  readonly commits: CommitResource;
236
- agent(id: string, options: AgentOptions): Agent;
237
126
  model<T = Record<string, unknown>>(name: string): ModelClient<T>;
238
- beginTurn(options: TaskCreateOptions): Promise<Turn>;
239
127
  /**
240
128
  * Resolve the active bearer credential this client authenticates with — the
241
129
  * same token its own requests carry in `Authorization`. Returns `null` when