Code Mode for Mutations
@absolutejs/sync@1.10.0 adds a new /code-mode subpath that exposes the engine's mutation surface as a host-tool map shape-compatible with @absolutejs/ai's codeModeTool. N optimistic mutations collapse into one model turn — the agent emits a single function body that chains them through the sandbox, and only the final return enters the conversation context.
#Why one tool, not N
The Code Mode pattern was popularized by Cloudflare's Dynamic Workers (April 2026) and parallel work on Anthropic's programmatic tool calling. Both vendors report ~80% token reduction on multi-tool turns. Instead of exposing N tools and letting the model call them sequentially (N round-trips, N tool-call tokens, intermediate state crowding the context), Code Mode exposes one tool whose body is JS the model writes. The sandbox runs the JS, calls the host fns in any order, and returns one value.
Sync's contribution: the engine's mutation surface is the underlying host fn set. A mutation handler the engine already validates, retries, fans out, and authorizes becomes a function the model can call from a sandbox script. The integration is a single factory call.
#The host-tool factory
engineMutationsAsHostTools takes the engine, a per-call ctx() factory, and the mutation descriptor list. It returns a record shape-compatible with codeModeTool({ tools }) — no import of @absolutejs/ai from sync's side, so the AI SDK stays decoupled from the engine.
// @absolutejs/sync@1.10.0 — new /code-mode subpath.
import { createSyncEngine } from '@absolutejs/sync/engine';
import { engineMutationsAsHostTools } from '@absolutejs/sync/code-mode';
import { codeModeTool } from '@absolutejs/ai/tools';
const engine = createSyncEngine();
// ... registerMutation('comments:create', ...) etc. ...
// Wrap the engine's mutation surface as a Code Mode host-tool map.
const hostTools = engineMutationsAsHostTools<{ userId: string }>({
engine,
// Called ONCE per Code Mode invocation; threaded through every
// mutation in that script.
ctx: () => ({ userId: currentSessionUserId() }),
mutations: [
{
name: 'comments:create',
description: 'Post a comment on a resource.',
tsSignature:
'(args: { resourceId: string; body: string }) => ' +
'Promise<{ id: string; body: string; authorId: string }>',
},
{
name: 'comments:toggleReaction',
description: 'Toggle an emoji reaction on a comment.',
tsSignature:
'(args: { commentId: string; emoji: string }) => ' +
'Promise<{ added: boolean }>',
},
],
});
// Plug the host-tool map into ai's codeModeTool. The model sees ONE
// tool called `run_code`; its description is the auto-generated
// prompt listing every host fn with its TS signature.
const aiTool = codeModeTool({ tools: hostTools });#What the model emits
The model sees the typed signatures of every host fn in the prompt and writes ONE function body that chains them. A built-in log(...) is in scope for debugging — its messages come back alongside the result without entering the conversation context.
// What the model emits — one function body, multiple host calls,
// one returned value. The intermediate results never enter the
// conversation context.
const c = await comments_create({
resourceId: 'shared-discussion',
body: 'Looks great @alice!',
});
await comments_toggleReaction({ commentId: c.id, emoji: '🎉' });
log('done with', c.id);
return { commentId: c.id, body: c.body };#Host-fn naming
Engine mutation names use : because they're addressing strings, not identifiers. The host-tool map auto-derives JS-safe names by replacing non-identifier characters with _. Override per-mutation with hostFnName when the derived name would collide. Build-time errors surface at boot, not at the first model call.
// Engine mutation names use `:` (e.g. 'comments:create') because
// they're addressing, not identifiers. The host-tool map auto-derives
// JS-safe names by replacing non-identifier chars with `_`:
//
// 'comments:create' → comments_create
// 'comments:toggleReaction' → comments_toggleReaction
// 'favorites:togglePin' → favorites_togglePin
//
// Override per-mutation with `hostFnName` when the derived name would
// collide or when you want something terser for the model prompt:
{
name: 'comments:create',
description: 'Post a comment.',
hostFnName: 'post_comment',
tsSignature: '(args: { resourceId: string; body: string }) => Promise<Comment>',
}
// Build-time errors surface at boot, not at the first model call:
// - "mutation 'never:registered' is not registered on the engine"
// - "duplicate host-fn name 'comments_create'"#v0.1 semantics
Each runMutation call in the model's script runs in its own DB transaction — the engine's per-call retry + tx wrapper. If mutation 3/5 throws, mutations 1–2 are already committed. The model receives the error and can compensate in code, or surface the partial-success state for a follow-up turn.
When you need cross-mutation atomicity instead — all-or-nothing across N runMutations — skip the per-mutation host fns and reach for transactionalBatchAsHostTool + engine.runMutations in sync 1.11+ (see Atomic batches below). The per-mutation surface here ships honest v0.1 semantics; the batched surface ships the all-or-nothing variant. Pick per host fn, not per engine.
In practice: design the host fns so each is independently safe to commit. Idempotent mutations (favorites toggle on a deterministic row id; mentions record on a primary key composed of the source) make this easy.
// v0.1 partial-failure semantics — READ CAREFULLY.
//
// Each `runMutation` call in the model's script runs in its own DB
// transaction (the engine's per-call retry + tx wrapper). If
// mutation 3/5 throws, mutations 1–2 are already committed. The
// model gets the error and can decide whether to compensate (e.g.
// by calling a delete mutation in a follow-up turn).
//
// Cross-mutation atomicity (all-or-nothing across N runMutations) is
// NOT provided in v0.1. It would need a new engine batch primitive
// that holds one tx open across multiple handlers — a deliberate
// v0.2 followup. Shipping the v0.1 surface honestly beats promising
// transactional semantics the engine can't keep.
//
// In practice: design the host fns so each one is independently
// safe to commit. Idempotent mutations (e.g. favorites:toggle on
// a deterministic row id) make this easy.
// What the model sees if mutation 2 throws:
try {
const c = await comments_create({ resourceId: 'r', body: '...' });
await comments_toggleReaction({ commentId: c.id, emoji: '!!' });
// Throws — reaction was rejected (e.g. emoji not in allowlist).
} catch (e) {
log('rolling back:', e.message);
// The comment was committed. We have to undo it explicitly.
// (A delete mutation would go here in a real app.)
return { error: e.message, partiallyCommitted: ['comment'] };
}#Worked example
The examples/sync app's React page renders a CodeModePanel with a textarea, Run button, and live output. The default body chainscomments_create + comments_toggleReaction. The Playwright test fills the textarea with a marker body, clicks Run, and asserts both tool calls land plus the created comment row materializing in the live comments-with-author join collection — proving the mutation committed end-to-end through the engine, not just round- tripped through the sandbox.
// Worked example in examples/sync — the React page's CodeModePanel
// posts to /sync/code-mode/run. The server wires the host-tool
// factory + codeModeTool inside the route handler.
.post(
'/sync/code-mode/run',
async ({ body }) => {
ensureDemoUser(body.userId); // host-side bookkeeping
const hostTools = engineMutationsAsHostTools<{ userId: string }>({
engine,
ctx: () => ({ userId: body.userId }),
mutations: [
{ name: 'comments:create', description: '...', tsSignature: '...' },
{ name: 'comments:toggleReaction', description: '...', tsSignature: '...' },
{ name: 'favorites:toggle', description: '...', tsSignature: '...' },
],
});
const tool = codeModeTool({ timeout: 5000, tools: hostTools });
// codeModeTool returns a JSON string; parse for the client.
const raw = await tool.handler({ code: body.code });
return typeof raw === 'string' ? JSON.parse(raw) : raw;
},
{ body: t.Object({ code: t.String(), userId: t.String() }) },
);#Atomic batches (1.11+)
sync@1.11.0 closes the cross-mutation atomicity gap. The engine gains runMutations(specs, ctx): N mutations in one DB transaction, accumulated buffered changes that fan out as ONE live diff on commit, and full rollback on any thrown handler — no partial commits, no surprise per-mutation diffs. Per-mutation authorize still runs (inside the tx); per-mutation retry policies do not apply to batches.
The /code-mode subpath gains transactionalBatchAsHostTool, which returns one Code Mode host fn (by convention run_transaction) that takes an Array<{ name, args }> from the model. The model can use the per-mutation host fns when it needs to branch on intermediate results, OR call run_transaction when it needs all-or-nothing semantics. Drop both into the same codeModeTool({ tools }) map.
// Cross-mutation atomicity in sync 1.11+. Pair the per-mutation
// host fns with a transactional batch host fn:
import {
engineMutationsAsHostTools,
transactionalBatchAsHostTool,
} from '@absolutejs/sync/code-mode';
import { codeModeTool } from '@absolutejs/ai/tools';
const hostTools = {
// Per-mutation fns — for scripts that branch on intermediate results.
...engineMutationsAsHostTools({
engine,
ctx: () => ({ userId: currentUserId() }),
mutations: [
{ name: 'comments:create', description: '...', tsSignature: '...' },
{ name: 'comments:toggleReaction', description: '...', tsSignature: '...' },
],
}),
// Atomic batch — for all-or-nothing semantics.
run_transaction: transactionalBatchAsHostTool({
engine,
ctx: () => ({ userId: currentUserId() }),
allowedMutations: ['comments:create', 'comments:toggleReaction'],
}),
};
const tool = codeModeTool({ tools: hostTools });What the model emits for the atomic path:
// What the model emits when it wants atomicity. The batch is one
// DB transaction; any handler throwing rolls everything back.
// Trade-off: there are no intermediate results — the model can't
// branch on what one mutation returned. Use the per-mutation host
// fns for that, this batch for "commit all or none."
const results = await run_transaction([
{ name: 'comments:create',
args: { resourceId: 'r-1', body: 'hi @bob' } },
{ name: 'comments:toggleReaction',
args: { commentId: 'c-known-id', emoji: '🎉' } },
]);
return { ids: results.map((row) => row.id) };The trade-off the batch makes explicit: the model can't reference an intermediate result inside the spec list (it's data, not code). When the script needs to thread a value from mutation 1 into mutation 2's args, use the per-mutation host fns and accept partial-failure compensation. When it needs to commit several pre-shaped writes together or not at all, use run_transaction.