@abloatai/transaction 0.37.1 → 0.38.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/dist/ai-sdk/index.d.ts +3 -1
- package/dist/ai-sdk/index.d.ts.map +1 -1
- package/dist/ai-sdk/index.js +2 -1
- package/dist/ai-sdk/index.js.map +1 -1
- package/dist/ai-sdk/modelTools.d.ts +67 -0
- package/dist/ai-sdk/modelTools.d.ts.map +1 -0
- package/dist/ai-sdk/modelTools.js +89 -0
- package/dist/ai-sdk/modelTools.js.map +1 -0
- package/dist/ai-sdk/toolOptions.d.ts +8 -0
- package/dist/ai-sdk/toolOptions.d.ts.map +1 -0
- package/dist/ai-sdk/toolOptions.js +2 -0
- package/dist/ai-sdk/toolOptions.js.map +1 -0
- package/dist/ai-sdk/{coordinatedTool.d.ts → updateTool.d.ts} +21 -28
- package/dist/ai-sdk/updateTool.d.ts.map +1 -0
- package/dist/ai-sdk/{coordinatedTool.js → updateTool.js} +39 -50
- package/dist/ai-sdk/updateTool.js.map +1 -0
- package/dist/auth/capability.d.ts +6 -0
- package/dist/auth/capability.d.ts.map +1 -1
- package/dist/auth/capability.js +4 -0
- package/dist/auth/capability.js.map +1 -1
- package/dist/auth/capabilityLifecycle.d.ts +3 -0
- package/dist/auth/capabilityLifecycle.d.ts.map +1 -1
- package/dist/auth/identity.d.ts +6 -4
- package/dist/auth/identity.d.ts.map +1 -1
- package/dist/auth/identity.js +7 -11
- package/dist/auth/identity.js.map +1 -1
- package/dist/auth/schemas.d.ts +5 -5
- package/dist/auth/schemas.d.ts.map +1 -1
- package/dist/auth/schemas.js +5 -2
- package/dist/auth/schemas.js.map +1 -1
- package/dist/auth/sessionMint.d.ts.map +1 -1
- package/dist/auth/sessionMint.js +3 -0
- package/dist/auth/sessionMint.js.map +1 -1
- package/dist/branches.d.ts +251 -0
- package/dist/branches.d.ts.map +1 -0
- package/dist/branches.js +108 -0
- package/dist/branches.js.map +1 -0
- package/dist/coordination/awaitClaimGrant.d.ts +2 -0
- package/dist/coordination/awaitClaimGrant.d.ts.map +1 -1
- package/dist/coordination/awaitClaimGrant.js +15 -2
- package/dist/coordination/awaitClaimGrant.js.map +1 -1
- package/dist/coordination/schema.d.ts +2 -0
- package/dist/coordination/schema.d.ts.map +1 -1
- package/dist/coordination/schema.js +2 -0
- package/dist/coordination/schema.js.map +1 -1
- package/dist/docs/catalog.d.ts +5 -5
- package/dist/docs/catalog.d.ts.map +1 -1
- package/dist/docs/catalog.js +12 -12
- package/dist/docs/catalog.js.map +1 -1
- package/dist/footprint.d.ts +3 -5
- package/dist/footprint.d.ts.map +1 -1
- package/dist/footprint.js +0 -0
- package/dist/footprint.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/keys/index.d.ts +15 -7
- package/dist/keys/index.d.ts.map +1 -1
- package/dist/keys/index.js +45 -14
- package/dist/keys/index.js.map +1 -1
- package/dist/schema/openapi.d.ts +1 -1
- package/dist/schema/openapi.d.ts.map +1 -1
- package/dist/schema/openapi.js +308 -29
- package/dist/schema/openapi.js.map +1 -1
- package/dist/server/commit.d.ts +2 -15
- package/dist/server/commit.d.ts.map +1 -1
- package/dist/source/adapter.d.ts +7 -2
- package/dist/source/adapter.d.ts.map +1 -1
- package/dist/source/adapter.js +4 -2
- package/dist/source/adapter.js.map +1 -1
- package/dist/source/adapterFactory.d.ts +10 -0
- package/dist/source/adapterFactory.d.ts.map +1 -0
- package/dist/source/adapterFactory.js +33 -0
- package/dist/source/adapterFactory.js.map +1 -0
- package/dist/source/adapterProfile.d.ts +55 -0
- package/dist/source/adapterProfile.d.ts.map +1 -0
- package/dist/source/adapterProfile.js +54 -0
- package/dist/source/adapterProfile.js.map +1 -0
- package/dist/source/adapters/drizzle.d.ts.map +1 -1
- package/dist/source/adapters/drizzle.js +5 -2
- package/dist/source/adapters/drizzle.js.map +1 -1
- package/dist/source/adapters/kysely.d.ts.map +1 -1
- package/dist/source/adapters/kysely.js +7 -4
- package/dist/source/adapters/kysely.js.map +1 -1
- package/dist/source/adapters/memory.d.ts.map +1 -1
- package/dist/source/adapters/memory.js +5 -2
- package/dist/source/adapters/memory.js.map +1 -1
- package/dist/source/adapters/prisma.d.ts.map +1 -1
- package/dist/source/adapters/prisma.js +5 -2
- package/dist/source/adapters/prisma.js.map +1 -1
- package/dist/source/conformance.d.ts.map +1 -1
- package/dist/source/conformance.js +18 -0
- package/dist/source/conformance.js.map +1 -1
- package/dist/source/index.d.ts +2 -0
- package/dist/source/index.d.ts.map +1 -1
- package/dist/source/index.js +2 -0
- package/dist/source/index.js.map +1 -1
- package/dist/source/types.d.ts +3 -14
- package/dist/source/types.d.ts.map +1 -1
- package/dist/source/types.js.map +1 -1
- package/dist/syncLog/contract.d.ts +1 -1
- package/dist/syncLog/contract.d.ts.map +1 -1
- package/dist/syncLog/contract.js +1 -2
- package/dist/syncLog/contract.js.map +1 -1
- package/dist/wire/accountResponses.d.ts +5 -9
- package/dist/wire/accountResponses.d.ts.map +1 -1
- package/dist/wire/accountResponses.js +5 -6
- package/dist/wire/accountResponses.js.map +1 -1
- package/dist/wire/delta.d.ts +11 -0
- package/dist/wire/delta.d.ts.map +1 -1
- package/dist/wire/delta.js +11 -0
- package/dist/wire/delta.js.map +1 -1
- package/dist/wire/inboundFrames.d.ts +2 -0
- package/dist/wire/inboundFrames.d.ts.map +1 -1
- package/package.json +8 -2
- package/src/ai-sdk/index.ts +19 -6
- package/src/ai-sdk/modelTools.ts +165 -0
- package/src/ai-sdk/toolOptions.ts +18 -0
- package/src/ai-sdk/{coordinatedTool.ts → updateTool.ts} +59 -69
- package/src/auth/capability.ts +4 -0
- package/src/auth/identity.ts +16 -16
- package/src/auth/schemas.ts +5 -2
- package/src/auth/sessionMint.ts +3 -0
- package/src/branches.ts +134 -0
- package/src/coordination/awaitClaimGrant.ts +18 -2
- package/src/coordination/schema.ts +2 -0
- package/src/docs/catalog.ts +24 -12
- package/src/footprint.ts +0 -0
- package/src/index.ts +1 -0
- package/src/keys/index.ts +56 -17
- package/src/schema/openapi.ts +366 -38
- package/src/server/commit.ts +2 -15
- package/src/source/adapter.ts +7 -2
- package/src/source/adapterFactory.ts +60 -0
- package/src/source/adapterProfile.ts +75 -0
- package/src/source/adapters/drizzle.ts +5 -2
- package/src/source/adapters/kysely.ts +10 -4
- package/src/source/adapters/memory.ts +5 -2
- package/src/source/adapters/prisma.ts +5 -2
- package/src/source/conformance.ts +18 -0
- package/src/source/index.ts +14 -0
- package/src/source/types.ts +3 -14
- package/src/syncLog/contract.ts +1 -2
- package/src/wire/accountResponses.ts +5 -6
- package/src/wire/delta.ts +11 -0
- package/dist/ai-sdk/coordinatedTool.d.ts.map +0 -1
- package/dist/ai-sdk/coordinatedTool.js.map +0 -1
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small AI SDK adapters for Ablo model resources.
|
|
3
|
+
*
|
|
4
|
+
* The AI SDK owns the agent loop. These helpers only expose Ablo's
|
|
5
|
+
* authoritative model operations as tools, preserving the same credential,
|
|
6
|
+
* validation, idempotency, claims, and settlement behavior as direct SDK use.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { tool, type ToolExecutionOptions } from 'ai';
|
|
10
|
+
import type { z } from 'zod';
|
|
11
|
+
import type {
|
|
12
|
+
ClaimParams,
|
|
13
|
+
ModelCreateParams,
|
|
14
|
+
ModelDeleteParams,
|
|
15
|
+
ModelRetrieveParams,
|
|
16
|
+
} from '../resources/modelOperations.js';
|
|
17
|
+
import type { HeldClaim } from '../types/streams.js';
|
|
18
|
+
import type { ModelToolOptions } from './toolOptions.js';
|
|
19
|
+
|
|
20
|
+
export interface ToolModel<T, CreateInput = Partial<T>, Fields = T> {
|
|
21
|
+
get(params: ModelRetrieveParams): Promise<T | undefined>;
|
|
22
|
+
create(params: ModelCreateParams<T, CreateInput>): Promise<T>;
|
|
23
|
+
delete(params: ModelDeleteParams<T, Fields>): Promise<void>;
|
|
24
|
+
claim(
|
|
25
|
+
params: ClaimParams<Fields> & { queue: false },
|
|
26
|
+
): Promise<HeldClaim<T> | null>;
|
|
27
|
+
claim(params: ClaimParams<Fields>): Promise<HeldClaim<T>>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ReadToolOptions<TInput, T>
|
|
31
|
+
extends ModelToolOptions<TInput, ReadToolResult<T>> {
|
|
32
|
+
readonly description: string;
|
|
33
|
+
readonly inputSchema: z.ZodType<TInput>;
|
|
34
|
+
readonly id: (input: TInput) => string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type ReadToolResult<T> =
|
|
38
|
+
| { readonly status: 'found'; readonly row: T }
|
|
39
|
+
| { readonly status: 'not_found'; readonly message: string };
|
|
40
|
+
|
|
41
|
+
export function readTool<TInput, T>(
|
|
42
|
+
model: Pick<ToolModel<T>, 'get'>,
|
|
43
|
+
options: ReadToolOptions<TInput, T>,
|
|
44
|
+
) {
|
|
45
|
+
return tool<TInput, ReadToolResult<T>>({
|
|
46
|
+
description: options.description,
|
|
47
|
+
title: options.title,
|
|
48
|
+
inputSchema: options.inputSchema,
|
|
49
|
+
inputExamples: options.inputExamples,
|
|
50
|
+
needsApproval: options.needsApproval,
|
|
51
|
+
strict: options.strict,
|
|
52
|
+
outputSchema: options.outputSchema,
|
|
53
|
+
toModelOutput: options.toModelOutput,
|
|
54
|
+
execute: async (input) => {
|
|
55
|
+
const id = options.id(input);
|
|
56
|
+
const row = await model.get({ id });
|
|
57
|
+
return row === undefined
|
|
58
|
+
? {
|
|
59
|
+
status: 'not_found',
|
|
60
|
+
message: `No accessible row exists with id ${id}.`,
|
|
61
|
+
}
|
|
62
|
+
: { status: 'found', row };
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface CreateToolOptions<TInput, CreateInput, T>
|
|
68
|
+
extends ModelToolOptions<TInput, CreateToolResult<T>> {
|
|
69
|
+
readonly description: string;
|
|
70
|
+
readonly inputSchema: z.ZodType<TInput>;
|
|
71
|
+
readonly data: (input: TInput) => CreateInput;
|
|
72
|
+
/**
|
|
73
|
+
* Stable ids make retries idempotent. Omit only when the model or server
|
|
74
|
+
* generates the id and duplicate creation is acceptable to the application.
|
|
75
|
+
*/
|
|
76
|
+
readonly id?: (input: TInput) => string | undefined;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface CreateToolResult<T> {
|
|
80
|
+
readonly status: 'created';
|
|
81
|
+
readonly row: T;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function createTool<TInput, T, CreateInput>(
|
|
85
|
+
model: Pick<ToolModel<T, CreateInput>, 'create'>,
|
|
86
|
+
options: CreateToolOptions<TInput, CreateInput, T>,
|
|
87
|
+
) {
|
|
88
|
+
return tool<TInput, CreateToolResult<T>>({
|
|
89
|
+
description: options.description,
|
|
90
|
+
title: options.title,
|
|
91
|
+
inputSchema: options.inputSchema,
|
|
92
|
+
inputExamples: options.inputExamples,
|
|
93
|
+
needsApproval: options.needsApproval,
|
|
94
|
+
strict: options.strict,
|
|
95
|
+
outputSchema: options.outputSchema,
|
|
96
|
+
toModelOutput: options.toModelOutput,
|
|
97
|
+
execute: async (input) => {
|
|
98
|
+
const id = options.id?.(input);
|
|
99
|
+
const row = await model.create({
|
|
100
|
+
data: options.data(input),
|
|
101
|
+
...(id !== undefined ? { id } : {}),
|
|
102
|
+
wait: 'confirmed',
|
|
103
|
+
});
|
|
104
|
+
return { status: 'created', row };
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export interface DeleteToolOptions<TInput>
|
|
110
|
+
extends ModelToolOptions<TInput, DeleteToolResult> {
|
|
111
|
+
readonly description: string;
|
|
112
|
+
readonly inputSchema: z.ZodType<TInput>;
|
|
113
|
+
readonly id: (input: TInput) => string;
|
|
114
|
+
/** Defaults to waiting in the server-owned FIFO claim queue. */
|
|
115
|
+
readonly strategy?: 'claim' | 'queue';
|
|
116
|
+
readonly claim?: { readonly description?: string };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type DeleteToolResult =
|
|
120
|
+
| { readonly status: 'deleted'; readonly id: string }
|
|
121
|
+
| { readonly status: 'claimed'; readonly message: string };
|
|
122
|
+
|
|
123
|
+
export function deleteTool<TInput, T, Fields = T>(
|
|
124
|
+
model: Pick<ToolModel<T, Partial<T>, Fields>, 'claim' | 'delete'>,
|
|
125
|
+
options: DeleteToolOptions<TInput>,
|
|
126
|
+
) {
|
|
127
|
+
return tool<TInput, DeleteToolResult>({
|
|
128
|
+
description: options.description,
|
|
129
|
+
title: options.title,
|
|
130
|
+
inputSchema: options.inputSchema,
|
|
131
|
+
inputExamples: options.inputExamples,
|
|
132
|
+
// Destructive tools require approval unless an application deliberately
|
|
133
|
+
// opts into autonomous deletion.
|
|
134
|
+
needsApproval: options.needsApproval ?? true,
|
|
135
|
+
strict: options.strict,
|
|
136
|
+
outputSchema: options.outputSchema,
|
|
137
|
+
toModelOutput: options.toModelOutput,
|
|
138
|
+
execute: async (
|
|
139
|
+
input: TInput,
|
|
140
|
+
execution: ToolExecutionOptions,
|
|
141
|
+
): Promise<DeleteToolResult> => {
|
|
142
|
+
const id = options.id(input);
|
|
143
|
+
const queue = options.strategy !== 'claim';
|
|
144
|
+
const claim = await model.claim({
|
|
145
|
+
id,
|
|
146
|
+
queue,
|
|
147
|
+
description: options.claim?.description,
|
|
148
|
+
signal: execution.abortSignal,
|
|
149
|
+
});
|
|
150
|
+
if (!claim) {
|
|
151
|
+
return {
|
|
152
|
+
status: 'claimed',
|
|
153
|
+
message:
|
|
154
|
+
'Another participant holds this row right now — it was not deleted.',
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
try {
|
|
158
|
+
await model.delete({ id, claim, wait: 'confirmed' });
|
|
159
|
+
return { status: 'deleted', id };
|
|
160
|
+
} finally {
|
|
161
|
+
await claim.release();
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Tool } from 'ai';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AI SDK-native presentation and execution policy shared by Ablo model tools.
|
|
5
|
+
* Ablo owns the data operation; AI SDK owns how the tool is presented,
|
|
6
|
+
* validated, approved, and returned to the model.
|
|
7
|
+
*/
|
|
8
|
+
export type ModelToolOptions<TInput, TResult> = Partial<
|
|
9
|
+
Pick<
|
|
10
|
+
Tool<TInput, TResult>,
|
|
11
|
+
| 'title'
|
|
12
|
+
| 'inputExamples'
|
|
13
|
+
| 'needsApproval'
|
|
14
|
+
| 'strict'
|
|
15
|
+
| 'outputSchema'
|
|
16
|
+
| 'toModelOutput'
|
|
17
|
+
>
|
|
18
|
+
>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Turns a write to one of your models into
|
|
2
|
+
* Turns a write to one of your models into an AI SDK tool that handles
|
|
3
3
|
* multi-agent coordination for you, so an agent can contribute to shared state
|
|
4
4
|
* without silently overwriting another writer's concurrent change.
|
|
5
5
|
*
|
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
* write:
|
|
12
12
|
*
|
|
13
13
|
* ```ts
|
|
14
|
-
* import {
|
|
14
|
+
* import { updateTool } from '@abloatai/ablo/ai-sdk';
|
|
15
15
|
* import { z } from 'zod';
|
|
16
16
|
*
|
|
17
|
-
* const saveSection =
|
|
17
|
+
* const saveSection = updateTool(ablo.documents, {
|
|
18
18
|
* description: 'Save your section into the shared document.',
|
|
19
19
|
* inputSchema: z.object({ text: z.string() }),
|
|
20
20
|
* id: () => DOC_ID,
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* await streamText({ model, messages, tools: { saveSection } });
|
|
26
26
|
* ```
|
|
27
27
|
*
|
|
28
|
-
* The {@link
|
|
28
|
+
* The {@link UpdateToolOptions.apply} function is the whole API: a pure
|
|
29
29
|
* function from the freshest row and the tool input to a patch, in the same
|
|
30
30
|
* spirit as a functional state update. Everything beneath it — reading the latest
|
|
31
31
|
* row, the compare-and-swap, backing off between retries, and releasing claims —
|
|
@@ -52,42 +52,39 @@
|
|
|
52
52
|
* than a hidden wait when it might spend its turn on something else. Works under
|
|
53
53
|
* any conflict policy.
|
|
54
54
|
*
|
|
55
|
-
* - `'queue'`
|
|
56
|
-
* the
|
|
57
|
-
*
|
|
58
|
-
* first-in-first-out, which would require a persistent connection.
|
|
55
|
+
* - `'queue'` joins Ablo's server-owned FIFO queue. The model calls once and
|
|
56
|
+
* the tool waits for the canonical claim grant; it does not recreate queueing
|
|
57
|
+
* with client-side polling.
|
|
59
58
|
*/
|
|
60
59
|
|
|
61
|
-
import { tool } from 'ai';
|
|
60
|
+
import { tool, type ToolExecutionOptions } from 'ai';
|
|
62
61
|
import type { z } from 'zod';
|
|
63
|
-
import { AbloClaimedError, AbloNotFoundError } from '@abloatai/transaction/errors';
|
|
64
62
|
import type {
|
|
65
63
|
ClaimParams,
|
|
66
|
-
ModelRetrieveParams,
|
|
67
64
|
ModelUpdateParams,
|
|
68
65
|
} from '../resources/modelOperations.js';
|
|
69
66
|
import type { ModelUpdater, ContentionOptions } from '../resources/functionalUpdate.js';
|
|
70
67
|
import type { HeldClaim } from '../types/streams.js';
|
|
68
|
+
import type { ModelToolOptions } from './toolOptions.js';
|
|
71
69
|
|
|
72
|
-
export type
|
|
70
|
+
export type UpdateStrategy = 'merge' | 'claim' | 'queue';
|
|
73
71
|
|
|
74
72
|
/** The structured result the tool hands back to the model (or the caller). */
|
|
75
|
-
export interface
|
|
73
|
+
export interface UpdateToolResult<T> {
|
|
76
74
|
/**
|
|
77
75
|
* `'written'` means the change was saved. `'claimed'` means another participant
|
|
78
76
|
* holds the row, so nothing was saved and the model should try again.
|
|
79
|
-
* `'timeout'` means the `'queue'` strategy could not acquire the row within
|
|
80
|
-
* `poll.timeoutMs`.
|
|
81
77
|
*/
|
|
82
|
-
status: 'written' | 'claimed'
|
|
78
|
+
status: 'written' | 'claimed';
|
|
83
79
|
/** The reconciled row, on `'written'`. */
|
|
84
80
|
row?: T;
|
|
85
81
|
message?: string;
|
|
86
|
-
/** On `'written'` via the `queue` strategy, how long the tool waited
|
|
82
|
+
/** On `'written'` via the `queue` strategy, how long the tool waited. */
|
|
87
83
|
waitedMs?: number;
|
|
88
84
|
}
|
|
89
85
|
|
|
90
|
-
export interface
|
|
86
|
+
export interface UpdateToolOptions<TInput, T>
|
|
87
|
+
extends ModelToolOptions<TInput, UpdateToolResult<T>> {
|
|
91
88
|
/** Tool description shown to the model. */
|
|
92
89
|
description: string;
|
|
93
90
|
/** The schema of what the model may send, as a standard AI SDK / Zod input schema. */
|
|
@@ -103,45 +100,50 @@ export interface CoordinatedToolOptions<TInput, T> {
|
|
|
103
100
|
*/
|
|
104
101
|
apply: (current: T, input: TInput) => Partial<T>;
|
|
105
102
|
/** How concurrent writers relate. Defaults to `'merge'`. */
|
|
106
|
-
strategy?:
|
|
103
|
+
strategy?: UpdateStrategy;
|
|
107
104
|
/** Human-readable coordination metadata attached to the claim, used by the `'claim'` and `'queue'` strategies. */
|
|
108
105
|
claim?: { description?: string };
|
|
109
106
|
/** How many reconcile rounds `'merge'` may take before it gives up with `AbloContentionError`. */
|
|
110
107
|
retries?: number;
|
|
111
|
-
/** Poll interval and overall timeout for `'queue'`. Defaults to 250ms and 30s. */
|
|
112
|
-
poll?: { intervalMs?: number; timeoutMs?: number };
|
|
113
108
|
}
|
|
114
109
|
|
|
115
|
-
const sleep = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
|
|
116
|
-
|
|
117
110
|
/**
|
|
118
111
|
* The small model port this helper needs. Both typed transports implement this
|
|
119
112
|
* exact contract; local-cache methods, wire receipts, and transport lifecycle
|
|
120
113
|
* deliberately stay out of an AI tool's dependency surface.
|
|
121
114
|
*/
|
|
122
|
-
export interface
|
|
123
|
-
get(params: ModelRetrieveParams): Promise<T | undefined>;
|
|
124
|
-
/** @deprecated Use `get({ id })`. */
|
|
125
|
-
retrieve?(params: ModelRetrieveParams): Promise<T | undefined>;
|
|
115
|
+
export interface UpdateToolModel<T> {
|
|
126
116
|
update(params: ModelUpdateParams<T>): Promise<T>;
|
|
127
117
|
update(
|
|
128
118
|
id: string,
|
|
129
119
|
updater: ModelUpdater<T>,
|
|
130
120
|
options?: ContentionOptions,
|
|
131
121
|
): Promise<T | undefined>;
|
|
122
|
+
claim(
|
|
123
|
+
params: ClaimParams<T> & { queue: false },
|
|
124
|
+
): Promise<HeldClaim<T> | null>;
|
|
132
125
|
claim(params: ClaimParams<T>): Promise<HeldClaim<T>>;
|
|
133
126
|
}
|
|
134
127
|
|
|
135
|
-
export function
|
|
128
|
+
export function updateTool<
|
|
136
129
|
TInput,
|
|
137
130
|
T = Record<string, unknown>,
|
|
138
|
-
>(model:
|
|
131
|
+
>(model: UpdateToolModel<T>, options: UpdateToolOptions<TInput, T>) {
|
|
139
132
|
const strategy = options.strategy ?? 'merge';
|
|
140
133
|
|
|
141
|
-
return tool<TInput,
|
|
134
|
+
return tool<TInput, UpdateToolResult<T>>({
|
|
142
135
|
description: options.description,
|
|
136
|
+
title: options.title,
|
|
143
137
|
inputSchema: options.inputSchema,
|
|
144
|
-
|
|
138
|
+
inputExamples: options.inputExamples,
|
|
139
|
+
needsApproval: options.needsApproval,
|
|
140
|
+
strict: options.strict,
|
|
141
|
+
outputSchema: options.outputSchema,
|
|
142
|
+
toModelOutput: options.toModelOutput,
|
|
143
|
+
execute: async (
|
|
144
|
+
input: TInput,
|
|
145
|
+
execution: ToolExecutionOptions,
|
|
146
|
+
): Promise<UpdateToolResult<T>> => {
|
|
145
147
|
const id = options.id(input);
|
|
146
148
|
|
|
147
149
|
if (strategy === 'merge') {
|
|
@@ -150,29 +152,39 @@ export function coordinatedTool<
|
|
|
150
152
|
// backoff. The model never sees a conflict.
|
|
151
153
|
const row = await model.update(id, (current) => options.apply(current, input), {
|
|
152
154
|
retries: options.retries,
|
|
155
|
+
signal: execution.abortSignal,
|
|
153
156
|
});
|
|
154
157
|
return { status: 'written', row: row ?? undefined };
|
|
155
158
|
}
|
|
156
159
|
|
|
157
160
|
// The 'claim' and 'queue' strategies both acquire a claim, write under it,
|
|
158
161
|
// and release it. They differ only in what they do when the row is already
|
|
159
|
-
// held: 'claim' returns a signal to the model, while 'queue' waits
|
|
160
|
-
//
|
|
161
|
-
const acquireWriteRelease = async (
|
|
162
|
+
// held: 'claim' returns a signal to the model, while 'queue' waits for
|
|
163
|
+
// the server-owned FIFO grant.
|
|
164
|
+
const acquireWriteRelease = async (
|
|
165
|
+
queue: boolean,
|
|
166
|
+
): Promise<UpdateToolResult<T>> => {
|
|
162
167
|
const claim = await model.claim({
|
|
163
168
|
id,
|
|
164
|
-
queue
|
|
169
|
+
queue,
|
|
165
170
|
description: options.claim?.description,
|
|
171
|
+
signal: execution.abortSignal,
|
|
166
172
|
});
|
|
173
|
+
if (!claim) {
|
|
174
|
+
return {
|
|
175
|
+
status: 'claimed',
|
|
176
|
+
message:
|
|
177
|
+
'Another participant holds this row right now — it was not saved.',
|
|
178
|
+
};
|
|
179
|
+
}
|
|
167
180
|
try {
|
|
168
|
-
const current =
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
const row = await model.update({ id, data: options.apply(current, input), claim, wait: 'confirmed' });
|
|
181
|
+
const current = claim.data;
|
|
182
|
+
const row = await model.update({
|
|
183
|
+
id,
|
|
184
|
+
data: options.apply(current, input),
|
|
185
|
+
claim,
|
|
186
|
+
wait: 'confirmed',
|
|
187
|
+
});
|
|
176
188
|
return { status: 'written', row };
|
|
177
189
|
} finally {
|
|
178
190
|
await claim.release();
|
|
@@ -180,35 +192,13 @@ export function coordinatedTool<
|
|
|
180
192
|
};
|
|
181
193
|
|
|
182
194
|
if (strategy === 'claim') {
|
|
183
|
-
|
|
184
|
-
return await acquireWriteRelease();
|
|
185
|
-
} catch (e) {
|
|
186
|
-
if (e instanceof AbloClaimedError) {
|
|
187
|
-
return { status: 'claimed', message: 'Another participant holds this row right now — it was NOT saved. Wait briefly and try again.' };
|
|
188
|
-
}
|
|
189
|
-
throw e;
|
|
190
|
-
}
|
|
195
|
+
return acquireWriteRelease(false);
|
|
191
196
|
}
|
|
192
197
|
|
|
193
|
-
//
|
|
194
|
-
const interval = options.poll?.intervalMs ?? 250;
|
|
195
|
-
const timeout = options.poll?.timeoutMs ?? 30_000;
|
|
198
|
+
// The transaction client owns the server FIFO and resolves once granted.
|
|
196
199
|
const start = Date.now();
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const result = await acquireWriteRelease();
|
|
200
|
-
return { ...result, waitedMs: Date.now() - start };
|
|
201
|
-
} catch (e) {
|
|
202
|
-
if (e instanceof AbloClaimedError) {
|
|
203
|
-
if (Date.now() - start >= timeout) {
|
|
204
|
-
return { status: 'timeout', message: `Could not acquire the row within ${timeout}ms.` };
|
|
205
|
-
}
|
|
206
|
-
await sleep(interval);
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
throw e;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
200
|
+
const result = await acquireWriteRelease(true);
|
|
201
|
+
return { ...result, waitedMs: Date.now() - start };
|
|
212
202
|
},
|
|
213
203
|
});
|
|
214
204
|
}
|
package/src/auth/capability.ts
CHANGED
|
@@ -239,6 +239,10 @@ export function grantedOperations(
|
|
|
239
239
|
*/
|
|
240
240
|
export const capabilityScopeSchema = z.object({
|
|
241
241
|
organizationId: z.string().min(1),
|
|
242
|
+
/** Credential target. Branch id is authoritative; null supports self-hosted identities. */
|
|
243
|
+
projectId: z.string().min(1).nullable().default(null),
|
|
244
|
+
branchId: z.string().min(1).nullable().default(null),
|
|
245
|
+
branchRoot: z.boolean().default(false),
|
|
242
246
|
/**
|
|
243
247
|
* The ROW axis — which sync groups this credential may act within. Read back
|
|
244
248
|
* as plain strings rather than the branded form the request enforces: this is
|
package/src/auth/identity.ts
CHANGED
|
@@ -42,7 +42,11 @@ export interface IdentityResolveInput {
|
|
|
42
42
|
readonly agentId?: string;
|
|
43
43
|
readonly syncGroups?: string[];
|
|
44
44
|
};
|
|
45
|
-
readonly internalOptions: {
|
|
45
|
+
readonly internalOptions: {
|
|
46
|
+
readonly organizationId?: string;
|
|
47
|
+
readonly branchId?: string;
|
|
48
|
+
readonly branchRoot?: boolean;
|
|
49
|
+
};
|
|
46
50
|
readonly url: string;
|
|
47
51
|
readonly kind: ParticipantKind;
|
|
48
52
|
readonly configuredApiKey: string | CredentialProvider | null;
|
|
@@ -57,10 +61,10 @@ export interface ResolvedIdentity {
|
|
|
57
61
|
readonly accountScope: string;
|
|
58
62
|
/** Concrete project selected by the credential. The org id is the default project. */
|
|
59
63
|
readonly projectId: string | null;
|
|
60
|
-
/**
|
|
61
|
-
readonly
|
|
62
|
-
/**
|
|
63
|
-
readonly
|
|
64
|
+
/** Immutable transaction branch selected by the credential. */
|
|
65
|
+
readonly branchId: string | null;
|
|
66
|
+
/** True only when branchId is the project's production root. */
|
|
67
|
+
readonly branchRoot: boolean;
|
|
64
68
|
readonly teamIds: string[] | undefined;
|
|
65
69
|
readonly capabilityToken: string | undefined;
|
|
66
70
|
readonly syncGroups: readonly string[] | undefined;
|
|
@@ -182,8 +186,8 @@ export async function resolveParticipantIdentity(
|
|
|
182
186
|
userId,
|
|
183
187
|
accountScope,
|
|
184
188
|
projectId: accountScope,
|
|
185
|
-
|
|
186
|
-
|
|
189
|
+
branchId: internalOptions.branchId ?? null,
|
|
190
|
+
branchRoot: internalOptions.branchRoot ?? false,
|
|
187
191
|
teamIds: kind === 'user' ? options.user?.teamIds : undefined,
|
|
188
192
|
capabilityToken: cred.getBearer,
|
|
189
193
|
syncGroups: options.syncGroups,
|
|
@@ -232,8 +236,8 @@ async function resolveViaIdentity(
|
|
|
232
236
|
userId: identity.participantId,
|
|
233
237
|
accountScope: identity.accountScope,
|
|
234
238
|
projectId: identity.projectId ?? identity.accountScope,
|
|
235
|
-
|
|
236
|
-
|
|
239
|
+
branchId: identity.branchId ?? null,
|
|
240
|
+
branchRoot: identity.branchRoot,
|
|
237
241
|
teamIds: undefined,
|
|
238
242
|
capabilityToken: bearer,
|
|
239
243
|
syncGroups: mergedSyncGroups,
|
|
@@ -312,13 +316,9 @@ async function resolveHosted(input: HostedInput): Promise<ResolvedIdentity> {
|
|
|
312
316
|
return {
|
|
313
317
|
userId: exchange.scope.participantId,
|
|
314
318
|
accountScope: exchange.scope.organizationId,
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
// therefore receive the concrete project/environment/sandbox tuple.
|
|
319
|
-
projectId: exchange.scope.organizationId,
|
|
320
|
-
environment: null,
|
|
321
|
-
sandboxId: null,
|
|
319
|
+
projectId: exchange.scope.projectId ?? exchange.scope.organizationId,
|
|
320
|
+
branchId: exchange.scope.branchId,
|
|
321
|
+
branchRoot: exchange.scope.branchRoot,
|
|
322
322
|
// teamIds isn't needed because the server already encoded
|
|
323
323
|
// team-level access into scope.syncGroups.
|
|
324
324
|
teamIds: undefined,
|
package/src/auth/schemas.ts
CHANGED
|
@@ -21,8 +21,8 @@ export const IdentityResolveResponseSchema = z.object({
|
|
|
21
21
|
// The rest of the resolved plane. The server always emits these keys; null
|
|
22
22
|
// means the credential does not bind that axis.
|
|
23
23
|
projectId: z.string().min(1).nullable(),
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
branchId: z.string().min(1).nullable().default(null),
|
|
25
|
+
branchRoot: z.boolean().default(false),
|
|
26
26
|
syncGroups: z.array(z.string()),
|
|
27
27
|
userMeta: z.record(z.string(), z.unknown()),
|
|
28
28
|
});
|
|
@@ -42,6 +42,9 @@ export const EphemeralKeyResponseSchema = z.object({
|
|
|
42
42
|
expiresAt: z.string().min(1),
|
|
43
43
|
organizationId: z.string().min(1),
|
|
44
44
|
participantId: z.string().min(1),
|
|
45
|
+
projectId: z.string().min(1).nullable().default(null),
|
|
46
|
+
branchId: z.string().min(1).nullable().default(null),
|
|
47
|
+
branchRoot: z.boolean().default(false),
|
|
45
48
|
syncGroups: z.array(z.string()),
|
|
46
49
|
/** Effective operation grant stored on the credential. */
|
|
47
50
|
operations: z.array(grantedOperationSchema).min(1),
|
package/src/auth/sessionMint.ts
CHANGED
|
@@ -91,6 +91,9 @@ export async function mintSession<S extends SchemaRecord>(
|
|
|
91
91
|
// response into the session resource's scope block.
|
|
92
92
|
scope: {
|
|
93
93
|
organizationId: res.organizationId,
|
|
94
|
+
projectId: res.projectId,
|
|
95
|
+
branchId: res.branchId,
|
|
96
|
+
branchRoot: res.branchRoot,
|
|
94
97
|
syncGroups: res.syncGroups,
|
|
95
98
|
operations: res.operations,
|
|
96
99
|
participantKind: 'user',
|
package/src/branches.ts
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A branch handle is for people and URLs. Durable routing always uses the
|
|
5
|
+
* immutable branch id returned by the server.
|
|
6
|
+
*/
|
|
7
|
+
export const branchSlugSchema = z
|
|
8
|
+
.string()
|
|
9
|
+
.min(1)
|
|
10
|
+
// During the additive rollout this is also the legacy plane adapter, whose
|
|
11
|
+
// persisted environment contract is capped at 40 characters.
|
|
12
|
+
.max(40)
|
|
13
|
+
.regex(
|
|
14
|
+
/^[a-z0-9](?:[a-z0-9-]{0,38}[a-z0-9])?$/,
|
|
15
|
+
'must be a lowercase slug containing only letters, numbers, and hyphens',
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
export const branchKindSchema = z.enum(['dev', 'preview', 'test', 'long_lived']);
|
|
19
|
+
export type BranchKind = z.infer<typeof branchKindSchema>;
|
|
20
|
+
|
|
21
|
+
export const branchStateSchema = z.enum([
|
|
22
|
+
'provisioning',
|
|
23
|
+
'ready',
|
|
24
|
+
'failed',
|
|
25
|
+
'deleting',
|
|
26
|
+
'deleted',
|
|
27
|
+
]);
|
|
28
|
+
export type BranchState = z.infer<typeof branchStateSchema>;
|
|
29
|
+
|
|
30
|
+
export const branchOriginSchema = z.enum(['empty', 'source_snapshot', 'coordinated_fork']);
|
|
31
|
+
export type BranchOrigin = z.infer<typeof branchOriginSchema>;
|
|
32
|
+
|
|
33
|
+
export const branchResponseSchema = z.object({
|
|
34
|
+
object: z.literal('branch'),
|
|
35
|
+
id: z.string(),
|
|
36
|
+
project_id: z.string(),
|
|
37
|
+
parent_branch_id: z.string().nullable(),
|
|
38
|
+
slug: branchSlugSchema,
|
|
39
|
+
name: z.string().nullable(),
|
|
40
|
+
kind: branchKindSchema,
|
|
41
|
+
state: branchStateSchema,
|
|
42
|
+
origin: branchOriginSchema,
|
|
43
|
+
root: z.boolean(),
|
|
44
|
+
expires_at: z.string().nullable(),
|
|
45
|
+
created_at: z.string(),
|
|
46
|
+
deleted_at: z.string().nullable(),
|
|
47
|
+
});
|
|
48
|
+
export type BranchResponse = z.infer<typeof branchResponseSchema>;
|
|
49
|
+
|
|
50
|
+
export const branchListResponseSchema = z.object({
|
|
51
|
+
object: z.literal('list'),
|
|
52
|
+
data: z.array(branchResponseSchema).readonly(),
|
|
53
|
+
});
|
|
54
|
+
export type BranchListResponse = z.infer<typeof branchListResponseSchema>;
|
|
55
|
+
|
|
56
|
+
export const createBranchRequestSchema = z.object({
|
|
57
|
+
slug: branchSlugSchema,
|
|
58
|
+
name: z.string().min(1).max(200).optional(),
|
|
59
|
+
parent_branch_id: z.string().optional(),
|
|
60
|
+
kind: branchKindSchema.optional(),
|
|
61
|
+
origin: branchOriginSchema.optional(),
|
|
62
|
+
expires_at: z.iso.datetime().optional(),
|
|
63
|
+
});
|
|
64
|
+
export type CreateBranchRequest = z.infer<typeof createBranchRequestSchema>;
|
|
65
|
+
|
|
66
|
+
export const branchCredentialRequestSchema = z
|
|
67
|
+
.object({
|
|
68
|
+
ttl_hours: z.number().int().min(1).max(168).optional(),
|
|
69
|
+
})
|
|
70
|
+
.strict();
|
|
71
|
+
export type BranchCredentialRequest = z.infer<typeof branchCredentialRequestSchema>;
|
|
72
|
+
|
|
73
|
+
export const branchCredentialResponseSchema = z.object({
|
|
74
|
+
object: z.literal('branch_credential'),
|
|
75
|
+
branch_id: z.string(),
|
|
76
|
+
api_key: z.string(),
|
|
77
|
+
expires_at: z.string(),
|
|
78
|
+
});
|
|
79
|
+
export type BranchCredentialResponse = z.infer<typeof branchCredentialResponseSchema>;
|
|
80
|
+
|
|
81
|
+
export const branchParentCompatibilitySchema = z.enum([
|
|
82
|
+
'same',
|
|
83
|
+
'compatible',
|
|
84
|
+
'review',
|
|
85
|
+
'blocked',
|
|
86
|
+
'unknown',
|
|
87
|
+
]);
|
|
88
|
+
export type BranchParentCompatibility = z.infer<typeof branchParentCompatibilitySchema>;
|
|
89
|
+
|
|
90
|
+
export const branchStatusSchemaSummarySchema = z
|
|
91
|
+
.object({
|
|
92
|
+
active: z.boolean(),
|
|
93
|
+
version: z.number().int().nullable(),
|
|
94
|
+
hash: z.string().nullable(),
|
|
95
|
+
parent_compatibility: branchParentCompatibilitySchema,
|
|
96
|
+
changes: z.number().int().nonnegative(),
|
|
97
|
+
warnings: z.number().int().nonnegative(),
|
|
98
|
+
blockers: z.number().int().nonnegative(),
|
|
99
|
+
})
|
|
100
|
+
.strict();
|
|
101
|
+
|
|
102
|
+
export const branchStatusDataSourceSchema = z
|
|
103
|
+
.object({
|
|
104
|
+
kind: z.enum(['hosted', 'direct', 'endpoint']),
|
|
105
|
+
status: z.enum(['unverified', 'active', 'rejected']).nullable(),
|
|
106
|
+
host: z.string().nullable(),
|
|
107
|
+
database: z.string().nullable(),
|
|
108
|
+
cursor: z.string().nullable(),
|
|
109
|
+
event_lag: z.number().int().nonnegative(),
|
|
110
|
+
retry_count: z.number().int().nonnegative(),
|
|
111
|
+
last_success_at: z.string().nullable(),
|
|
112
|
+
last_error: z.string().nullable(),
|
|
113
|
+
})
|
|
114
|
+
.strict();
|
|
115
|
+
|
|
116
|
+
export const branchStatusBlockerSchema = z
|
|
117
|
+
.object({
|
|
118
|
+
code: z.enum(['branch_not_ready', 'schema_missing', 'data_source_not_ready']),
|
|
119
|
+
problem: z.string(),
|
|
120
|
+
fix: z.string(),
|
|
121
|
+
})
|
|
122
|
+
.strict();
|
|
123
|
+
|
|
124
|
+
export const branchStatusResponseSchema = z
|
|
125
|
+
.object({
|
|
126
|
+
object: z.literal('branch_status'),
|
|
127
|
+
branch: branchResponseSchema,
|
|
128
|
+
ready: z.boolean(),
|
|
129
|
+
schema: branchStatusSchemaSummarySchema,
|
|
130
|
+
data_source: branchStatusDataSourceSchema,
|
|
131
|
+
blockers: z.array(branchStatusBlockerSchema).readonly(),
|
|
132
|
+
})
|
|
133
|
+
.strict();
|
|
134
|
+
export type BranchStatusResponse = z.infer<typeof branchStatusResponseSchema>;
|