@abloatai/ablo 0.23.0 → 0.24.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/CHANGELOG.md +20 -0
- package/dist/ai-sdk/coordinated-tool.d.ts +101 -0
- package/dist/ai-sdk/coordinated-tool.js +129 -0
- package/dist/ai-sdk/index.d.ts +42 -0
- package/dist/ai-sdk/index.js +42 -0
- package/dist/cli.cjs +687 -498
- package/dist/client/Ablo.js +4 -0
- package/dist/interfaces/index.d.ts +8 -0
- package/dist/sync/BootstrapHelper.d.ts +16 -0
- package/dist/sync/BootstrapHelper.js +27 -0
- package/dist/sync/schemas.d.ts +1 -0
- package/dist/sync/schemas.js +3 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.24.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Multi-agent coordination for AI SDK tools + safer `ablo push`.
|
|
8
|
+
- **`coordinatedTool` (`@abloatai/ablo/ai-sdk`)** — one call turns an Ablo model
|
|
9
|
+
write into a Vercel AI SDK `tool()` with concurrency coordination handled, so an
|
|
10
|
+
AI agent can contribute to shared state without clobbering concurrent writers.
|
|
11
|
+
Three strategies: `merge` (default — delegates to the functional update's
|
|
12
|
+
compare-and-swap + backoff, self-healing accumulate), `claim` (mutual exclusion,
|
|
13
|
+
returns a `claimed` signal the model retries on), and `queue` (SQS-style
|
|
14
|
+
poll-acquire over HTTP). The `ai-sdk` entry now also documents the canonical
|
|
15
|
+
multi-agent coordination model (surface-the-signal + back-off laws, strategy
|
|
16
|
+
table).
|
|
17
|
+
- **`ablo push` guards** — `--dry-run`/`--plan` prints the deploy target, a
|
|
18
|
+
model-level diff vs the deployed schema, and git state, then exits without
|
|
19
|
+
applying. Production deploys now require a typed confirmation (and refuse an
|
|
20
|
+
uncommitted schema unless `--allow-dirty`); sandbox confirms interactively.
|
|
21
|
+
`--yes`/`-y` skips confirmation for CI.
|
|
22
|
+
|
|
3
23
|
## 0.23.0
|
|
4
24
|
|
|
5
25
|
### Minor Changes
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `coordinatedTool` — the one-liner that turns an Ablo model write into a Vercel
|
|
3
|
+
* AI SDK tool with multi-agent coordination already handled, so an AI agent can
|
|
4
|
+
* contribute to shared state without ever silently clobbering a concurrent
|
|
5
|
+
* writer.
|
|
6
|
+
*
|
|
7
|
+
* The base `./ai-sdk` pattern (see index.ts) is "write your own `tool()` and
|
|
8
|
+
* call `ablo.<model>.update({ id, data, claim })` inside `execute`". That's the
|
|
9
|
+
* right amount of control when a tool does something bespoke. But the *common*
|
|
10
|
+
* case — "the agent produced some content; save it into the shared row" — should
|
|
11
|
+
* not require every integration to re-derive optimistic concurrency by hand. This
|
|
12
|
+
* collapses it to a declaration:
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { coordinatedTool } from '@abloatai/ablo/ai-sdk';
|
|
16
|
+
* import { z } from 'zod';
|
|
17
|
+
*
|
|
18
|
+
* const saveSection = coordinatedTool(ablo.documents, {
|
|
19
|
+
* description: 'Save your section into the shared document.',
|
|
20
|
+
* inputSchema: z.object({ text: z.string() }),
|
|
21
|
+
* id: () => DOC_ID,
|
|
22
|
+
* apply: (current, { text }) => ({ content: appendBlock(current.content, text) }),
|
|
23
|
+
* // strategy: 'merge' ← the default
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* await streamText({ model, messages, tools: { saveSection } });
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* `apply` is the whole API: a pure function of `(freshest row, tool input) →
|
|
30
|
+
* patch`, exactly like React's `setState(prev => next)`. Everything underneath —
|
|
31
|
+
* reading the latest row, the compare-and-swap, the jittered backoff between
|
|
32
|
+
* reconcile rounds, releasing claims — is the runtime's job, not yours.
|
|
33
|
+
*
|
|
34
|
+
* ## Strategies (pick by how writers should relate; all verified to converge
|
|
35
|
+
* under N-way agent contention)
|
|
36
|
+
*
|
|
37
|
+
* - `'merge'` *(default)* — delegates straight to the functional update
|
|
38
|
+
* `ablo.<model>.update(id, current => apply(current, input))`. The SDK re-reads
|
|
39
|
+
* and re-applies `apply` on top of every concurrent write and backs off between
|
|
40
|
+
* rounds, so N agents *accumulate* into one row and the model never sees a
|
|
41
|
+
* conflict. **Requires the model's agent conflict policy to be `reject`** (the
|
|
42
|
+
* default, or `agentsReject()`); a model declaring `agentsNotify()` HOLDS the
|
|
43
|
+
* losing write instead of rejecting it, which defeats the reconcile — use
|
|
44
|
+
* `claim`/`queue` there, or switch the policy.
|
|
45
|
+
*
|
|
46
|
+
* - `'claim'` — mutual exclusion. Takes a fail-fast claim; if another participant
|
|
47
|
+
* holds the row it returns `{ status: 'claimed' }` so the *model* decides to
|
|
48
|
+
* retry (a legible signal beats a hidden wait when the agent might do something
|
|
49
|
+
* better with its turn). Works regardless of conflict policy.
|
|
50
|
+
*
|
|
51
|
+
* - `'queue'` — fair-ish serialization over stateless HTTP, the SQS shape: a
|
|
52
|
+
* client poll-acquire loop (true FIFO needs a socket) until the claim is granted
|
|
53
|
+
* or `poll.timeoutMs` elapses. The model calls once and the tool waits its turn.
|
|
54
|
+
*/
|
|
55
|
+
import type { z } from 'zod';
|
|
56
|
+
import type { ModelOperations } from '../client/createModelProxy.js';
|
|
57
|
+
export type CoordinationStrategy = 'merge' | 'claim' | 'queue';
|
|
58
|
+
/** The structured result the tool hands back to the model (or the caller). */
|
|
59
|
+
export interface CoordinatedWriteResult<T> {
|
|
60
|
+
/**
|
|
61
|
+
* `'written'` — saved. `'claimed'` — another participant holds the row; NOT
|
|
62
|
+
* saved, the model should try again. `'timeout'` — the queue strategy could not
|
|
63
|
+
* acquire the row within `poll.timeoutMs`.
|
|
64
|
+
*/
|
|
65
|
+
status: 'written' | 'claimed' | 'timeout';
|
|
66
|
+
/** The reconciled row, on `'written'`. */
|
|
67
|
+
row?: T;
|
|
68
|
+
message?: string;
|
|
69
|
+
/** On `'written'` via the `queue` strategy, how long the tool waited in line. */
|
|
70
|
+
waitedMs?: number;
|
|
71
|
+
}
|
|
72
|
+
export interface CoordinatedToolOptions<TInput, T> {
|
|
73
|
+
/** Tool description shown to the model. */
|
|
74
|
+
description: string;
|
|
75
|
+
/** What the model may send — a normal AI SDK / zod input schema. */
|
|
76
|
+
inputSchema: z.ZodType<TInput>;
|
|
77
|
+
/** Which row this write targets, derived from the tool input. */
|
|
78
|
+
id: (input: TInput) => string;
|
|
79
|
+
/**
|
|
80
|
+
* Produce the write patch from the freshest current row + the tool input — a
|
|
81
|
+
* pure `(prev, input) => next`. Under `merge` it re-runs on every concurrent
|
|
82
|
+
* write, so it must be idempotent w.r.t. its own contribution (e.g. skip if its
|
|
83
|
+
* marker is already present) to be safe across reconcile rounds.
|
|
84
|
+
*/
|
|
85
|
+
apply: (current: T, input: TInput) => Partial<T>;
|
|
86
|
+
/** How concurrent writers relate. Defaults to `'merge'`. */
|
|
87
|
+
strategy?: CoordinationStrategy;
|
|
88
|
+
/** Human-legible coordination metadata attached to the claim (`claim`/`queue`). */
|
|
89
|
+
claim?: {
|
|
90
|
+
reason?: string;
|
|
91
|
+
description?: string;
|
|
92
|
+
};
|
|
93
|
+
/** Reconcile budget for `merge` (rounds before `AbloContentionError`). */
|
|
94
|
+
retries?: number;
|
|
95
|
+
/** Poll cadence / ceiling for `queue` (defaults 250ms / 30s). */
|
|
96
|
+
poll?: {
|
|
97
|
+
intervalMs?: number;
|
|
98
|
+
timeoutMs?: number;
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export declare function coordinatedTool<TInput, T = Record<string, unknown>, CreateInput = Partial<T>>(model: ModelOperations<T, CreateInput>, options: CoordinatedToolOptions<TInput, T>): import("ai").Tool<TInput, CoordinatedWriteResult<T>>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `coordinatedTool` — the one-liner that turns an Ablo model write into a Vercel
|
|
3
|
+
* AI SDK tool with multi-agent coordination already handled, so an AI agent can
|
|
4
|
+
* contribute to shared state without ever silently clobbering a concurrent
|
|
5
|
+
* writer.
|
|
6
|
+
*
|
|
7
|
+
* The base `./ai-sdk` pattern (see index.ts) is "write your own `tool()` and
|
|
8
|
+
* call `ablo.<model>.update({ id, data, claim })` inside `execute`". That's the
|
|
9
|
+
* right amount of control when a tool does something bespoke. But the *common*
|
|
10
|
+
* case — "the agent produced some content; save it into the shared row" — should
|
|
11
|
+
* not require every integration to re-derive optimistic concurrency by hand. This
|
|
12
|
+
* collapses it to a declaration:
|
|
13
|
+
*
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { coordinatedTool } from '@abloatai/ablo/ai-sdk';
|
|
16
|
+
* import { z } from 'zod';
|
|
17
|
+
*
|
|
18
|
+
* const saveSection = coordinatedTool(ablo.documents, {
|
|
19
|
+
* description: 'Save your section into the shared document.',
|
|
20
|
+
* inputSchema: z.object({ text: z.string() }),
|
|
21
|
+
* id: () => DOC_ID,
|
|
22
|
+
* apply: (current, { text }) => ({ content: appendBlock(current.content, text) }),
|
|
23
|
+
* // strategy: 'merge' ← the default
|
|
24
|
+
* });
|
|
25
|
+
*
|
|
26
|
+
* await streamText({ model, messages, tools: { saveSection } });
|
|
27
|
+
* ```
|
|
28
|
+
*
|
|
29
|
+
* `apply` is the whole API: a pure function of `(freshest row, tool input) →
|
|
30
|
+
* patch`, exactly like React's `setState(prev => next)`. Everything underneath —
|
|
31
|
+
* reading the latest row, the compare-and-swap, the jittered backoff between
|
|
32
|
+
* reconcile rounds, releasing claims — is the runtime's job, not yours.
|
|
33
|
+
*
|
|
34
|
+
* ## Strategies (pick by how writers should relate; all verified to converge
|
|
35
|
+
* under N-way agent contention)
|
|
36
|
+
*
|
|
37
|
+
* - `'merge'` *(default)* — delegates straight to the functional update
|
|
38
|
+
* `ablo.<model>.update(id, current => apply(current, input))`. The SDK re-reads
|
|
39
|
+
* and re-applies `apply` on top of every concurrent write and backs off between
|
|
40
|
+
* rounds, so N agents *accumulate* into one row and the model never sees a
|
|
41
|
+
* conflict. **Requires the model's agent conflict policy to be `reject`** (the
|
|
42
|
+
* default, or `agentsReject()`); a model declaring `agentsNotify()` HOLDS the
|
|
43
|
+
* losing write instead of rejecting it, which defeats the reconcile — use
|
|
44
|
+
* `claim`/`queue` there, or switch the policy.
|
|
45
|
+
*
|
|
46
|
+
* - `'claim'` — mutual exclusion. Takes a fail-fast claim; if another participant
|
|
47
|
+
* holds the row it returns `{ status: 'claimed' }` so the *model* decides to
|
|
48
|
+
* retry (a legible signal beats a hidden wait when the agent might do something
|
|
49
|
+
* better with its turn). Works regardless of conflict policy.
|
|
50
|
+
*
|
|
51
|
+
* - `'queue'` — fair-ish serialization over stateless HTTP, the SQS shape: a
|
|
52
|
+
* client poll-acquire loop (true FIFO needs a socket) until the claim is granted
|
|
53
|
+
* or `poll.timeoutMs` elapses. The model calls once and the tool waits its turn.
|
|
54
|
+
*/
|
|
55
|
+
import { tool } from 'ai';
|
|
56
|
+
import { AbloClaimedError, AbloNotFoundError } from '../errors.js';
|
|
57
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
58
|
+
export function coordinatedTool(model, options) {
|
|
59
|
+
const strategy = options.strategy ?? 'merge';
|
|
60
|
+
return tool({
|
|
61
|
+
description: options.description,
|
|
62
|
+
inputSchema: options.inputSchema,
|
|
63
|
+
execute: async (input) => {
|
|
64
|
+
const id = options.id(input);
|
|
65
|
+
if (strategy === 'merge') {
|
|
66
|
+
// The setState of the data layer: read fresh → apply → CAS → re-read +
|
|
67
|
+
// re-apply with backoff on any concurrent write. Self-healing; the model
|
|
68
|
+
// never sees a conflict. (Backoff lives in the shared reconcile loop.)
|
|
69
|
+
const row = await model.update(id, (current) => options.apply(current, input), {
|
|
70
|
+
retries: options.retries,
|
|
71
|
+
});
|
|
72
|
+
return { status: 'written', row: row ?? undefined };
|
|
73
|
+
}
|
|
74
|
+
// claim / queue both take a claim, write under it, and release. The only
|
|
75
|
+
// difference is what they do when the row is already held: claim returns the
|
|
76
|
+
// signal to the model; queue waits and retries (SQS-style poll-acquire).
|
|
77
|
+
const acquireWriteRelease = async () => {
|
|
78
|
+
const claim = await model.claim({
|
|
79
|
+
id,
|
|
80
|
+
queue: false,
|
|
81
|
+
reason: options.claim?.reason,
|
|
82
|
+
description: options.claim?.description,
|
|
83
|
+
});
|
|
84
|
+
try {
|
|
85
|
+
const current = await model.retrieve({ id });
|
|
86
|
+
if (current === undefined) {
|
|
87
|
+
throw new AbloNotFoundError(`Cannot write ${id}: it does not exist (or is outside this credential's scope).`, [id]);
|
|
88
|
+
}
|
|
89
|
+
const row = await model.update({ id, data: options.apply(current, input), claim, wait: 'confirmed' });
|
|
90
|
+
return { status: 'written', row };
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
await claim.release();
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
if (strategy === 'claim') {
|
|
97
|
+
try {
|
|
98
|
+
return await acquireWriteRelease();
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
if (e instanceof AbloClaimedError) {
|
|
102
|
+
return { status: 'claimed', message: 'Another participant holds this row right now — it was NOT saved. Wait briefly and try again.' };
|
|
103
|
+
}
|
|
104
|
+
throw e;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// strategy === 'queue': SQS-style poll-acquire over stateless HTTP.
|
|
108
|
+
const interval = options.poll?.intervalMs ?? 250;
|
|
109
|
+
const timeout = options.poll?.timeoutMs ?? 30_000;
|
|
110
|
+
const start = Date.now();
|
|
111
|
+
for (;;) {
|
|
112
|
+
try {
|
|
113
|
+
const result = await acquireWriteRelease();
|
|
114
|
+
return { ...result, waitedMs: Date.now() - start };
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
if (e instanceof AbloClaimedError) {
|
|
118
|
+
if (Date.now() - start >= timeout) {
|
|
119
|
+
return { status: 'timeout', message: `Could not acquire the row within ${timeout}ms.` };
|
|
120
|
+
}
|
|
121
|
+
await sleep(interval);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
throw e;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
}
|
package/dist/ai-sdk/index.d.ts
CHANGED
|
@@ -71,6 +71,48 @@
|
|
|
71
71
|
* `wrapWithMultiplayer` is optional. Use it when the whole model call is scoped
|
|
72
72
|
* to one entity before any tool is chosen; tool implementations stay exactly
|
|
73
73
|
* the same.
|
|
74
|
+
*
|
|
75
|
+
* ## Multi-agent coordination — the canonical way
|
|
76
|
+
*
|
|
77
|
+
* When several agents (or agents + humans) write the SAME row concurrently, the
|
|
78
|
+
* outcome is decided by **(write path) × (the model's conflict policy)**, NOT by
|
|
79
|
+
* how smart the model is. The same model silently loses 3 of 4 concurrent
|
|
80
|
+
* contributions through a blind whole-row write, and lands all 4 through a
|
|
81
|
+
* coordinated one — because the coordinated write returns a *signal* the model
|
|
82
|
+
* (or the runtime) acts on. Two empirical laws fall out:
|
|
83
|
+
*
|
|
84
|
+
* 1. **Surface the signal.** A write that swallows the conflict and reports
|
|
85
|
+
* success is the footgun. Every robust path returns a legible result
|
|
86
|
+
* (`reject` → re-read & retry; `claimed` → the model tries again) instead of
|
|
87
|
+
* clobbering. Reaching for a bigger model does not fix a silent write.
|
|
88
|
+
* 2. **Back off.** Under N-way contention, writers that retry in lock-step just
|
|
89
|
+
* re-collide. The shared reconcile loop already jitters its backoff; any
|
|
90
|
+
* hand-rolled retry must too, or it exhausts its budget and drops a writer.
|
|
91
|
+
*
|
|
92
|
+
* `coordinatedTool` (below) encodes both. Prefer it over a hand-written tool for
|
|
93
|
+
* "save the agent's contribution into the shared row":
|
|
94
|
+
*
|
|
95
|
+
* ```ts
|
|
96
|
+
* import { coordinatedTool } from '@abloatai/ablo/ai-sdk';
|
|
97
|
+
* const saveSection = coordinatedTool(ablo.documents, {
|
|
98
|
+
* description: 'Save your section into the shared document.',
|
|
99
|
+
* inputSchema: z.object({ text: z.string() }),
|
|
100
|
+
* id: () => DOC_ID,
|
|
101
|
+
* apply: (current, { text }) => ({ content: appendBlock(current.content, text) }),
|
|
102
|
+
* strategy: 'merge', // 'merge' (default, self-healing) | 'claim' | 'queue'
|
|
103
|
+
* });
|
|
104
|
+
* ```
|
|
105
|
+
*
|
|
106
|
+
* | strategy | writers relate by | on contention | model conflict policy |
|
|
107
|
+
* |----------|-------------------|---------------|------------------------|
|
|
108
|
+
* | `merge` | accumulate (CAS) | re-read + re-apply (silent, backed off) | must be `reject` (default) |
|
|
109
|
+
* | `claim` | mutual exclusion | returns `{status:'claimed'}` → model retries | any |
|
|
110
|
+
* | `queue` | FIFO-ish (SQS) | poll-acquire until granted / timeout | any |
|
|
111
|
+
*
|
|
112
|
+
* Note: a model declaring `agentsNotify()` HOLDS a losing write instead of
|
|
113
|
+
* rejecting it, which defeats `merge`'s reconcile (the loser is dropped, not
|
|
114
|
+
* retried). Use `agentsReject()` for accumulate semantics, or `claim`/`queue`.
|
|
74
115
|
*/
|
|
75
116
|
export { coordinationContextMiddleware, type CoordinationContextMiddlewareOptions, type ClaimTarget, } from './coordination-context.js';
|
|
76
117
|
export { wrapWithMultiplayer, type WrapWithMultiplayerOptions } from './wrap.js';
|
|
118
|
+
export { coordinatedTool, type CoordinationStrategy, type CoordinatedToolOptions, type CoordinatedWriteResult, } from './coordinated-tool.js';
|
package/dist/ai-sdk/index.js
CHANGED
|
@@ -71,6 +71,48 @@
|
|
|
71
71
|
* `wrapWithMultiplayer` is optional. Use it when the whole model call is scoped
|
|
72
72
|
* to one entity before any tool is chosen; tool implementations stay exactly
|
|
73
73
|
* the same.
|
|
74
|
+
*
|
|
75
|
+
* ## Multi-agent coordination — the canonical way
|
|
76
|
+
*
|
|
77
|
+
* When several agents (or agents + humans) write the SAME row concurrently, the
|
|
78
|
+
* outcome is decided by **(write path) × (the model's conflict policy)**, NOT by
|
|
79
|
+
* how smart the model is. The same model silently loses 3 of 4 concurrent
|
|
80
|
+
* contributions through a blind whole-row write, and lands all 4 through a
|
|
81
|
+
* coordinated one — because the coordinated write returns a *signal* the model
|
|
82
|
+
* (or the runtime) acts on. Two empirical laws fall out:
|
|
83
|
+
*
|
|
84
|
+
* 1. **Surface the signal.** A write that swallows the conflict and reports
|
|
85
|
+
* success is the footgun. Every robust path returns a legible result
|
|
86
|
+
* (`reject` → re-read & retry; `claimed` → the model tries again) instead of
|
|
87
|
+
* clobbering. Reaching for a bigger model does not fix a silent write.
|
|
88
|
+
* 2. **Back off.** Under N-way contention, writers that retry in lock-step just
|
|
89
|
+
* re-collide. The shared reconcile loop already jitters its backoff; any
|
|
90
|
+
* hand-rolled retry must too, or it exhausts its budget and drops a writer.
|
|
91
|
+
*
|
|
92
|
+
* `coordinatedTool` (below) encodes both. Prefer it over a hand-written tool for
|
|
93
|
+
* "save the agent's contribution into the shared row":
|
|
94
|
+
*
|
|
95
|
+
* ```ts
|
|
96
|
+
* import { coordinatedTool } from '@abloatai/ablo/ai-sdk';
|
|
97
|
+
* const saveSection = coordinatedTool(ablo.documents, {
|
|
98
|
+
* description: 'Save your section into the shared document.',
|
|
99
|
+
* inputSchema: z.object({ text: z.string() }),
|
|
100
|
+
* id: () => DOC_ID,
|
|
101
|
+
* apply: (current, { text }) => ({ content: appendBlock(current.content, text) }),
|
|
102
|
+
* strategy: 'merge', // 'merge' (default, self-healing) | 'claim' | 'queue'
|
|
103
|
+
* });
|
|
104
|
+
* ```
|
|
105
|
+
*
|
|
106
|
+
* | strategy | writers relate by | on contention | model conflict policy |
|
|
107
|
+
* |----------|-------------------|---------------|------------------------|
|
|
108
|
+
* | `merge` | accumulate (CAS) | re-read + re-apply (silent, backed off) | must be `reject` (default) |
|
|
109
|
+
* | `claim` | mutual exclusion | returns `{status:'claimed'}` → model retries | any |
|
|
110
|
+
* | `queue` | FIFO-ish (SQS) | poll-acquire until granted / timeout | any |
|
|
111
|
+
*
|
|
112
|
+
* Note: a model declaring `agentsNotify()` HOLDS a losing write instead of
|
|
113
|
+
* rejecting it, which defeats `merge`'s reconcile (the loser is dropped, not
|
|
114
|
+
* retried). Use `agentsReject()` for accumulate semantics, or `claim`/`queue`.
|
|
74
115
|
*/
|
|
75
116
|
export { coordinationContextMiddleware, } from './coordination-context.js';
|
|
76
117
|
export { wrapWithMultiplayer } from './wrap.js';
|
|
118
|
+
export { coordinatedTool, } from './coordinated-tool.js';
|