@absolutejs/sync 1.10.0 → 1.12.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/codeMode.d.ts +45 -0
- package/dist/codeMode.js +33 -1
- package/dist/codeMode.js.map +3 -3
- package/dist/engine/devtools.d.ts +9 -0
- package/dist/engine/index.js +72 -3
- package/dist/engine/index.js.map +4 -4
- package/dist/engine/sandbox.d.ts +46 -0
- package/dist/engine/syncEngine.d.ts +22 -0
- package/dist/index.js +72 -3
- package/dist/index.js.map +4 -4
- package/dist/testing.js +72 -3
- package/dist/testing.js.map +4 -4
- package/package.json +1 -1
package/dist/codeMode.d.ts
CHANGED
|
@@ -106,6 +106,24 @@ export type EngineMutationsAsHostToolsOptions<Ctx> = {
|
|
|
106
106
|
/** The mutation set to expose. */
|
|
107
107
|
mutations: MutationToolDescriptor[];
|
|
108
108
|
};
|
|
109
|
+
/** Options for {@link transactionalBatchAsHostTool}. */
|
|
110
|
+
export type TransactionalBatchOptions<Ctx> = {
|
|
111
|
+
/** The engine to call `runMutations` on. */
|
|
112
|
+
engine: SyncEngine;
|
|
113
|
+
/** Resolve per-call ctx — same shape as the other factory. */
|
|
114
|
+
ctx: () => Ctx | Promise<Ctx>;
|
|
115
|
+
/**
|
|
116
|
+
* Allowlist of mutation names the model may include in a batch.
|
|
117
|
+
* The host-fn checks every entry against this set before calling
|
|
118
|
+
* `runMutations`, so a hallucinated name fails fast with a clear
|
|
119
|
+
* error instead of bubbling up from the engine.
|
|
120
|
+
*/
|
|
121
|
+
allowedMutations: string[];
|
|
122
|
+
/** Override the model-visible host-fn description. */
|
|
123
|
+
description?: string;
|
|
124
|
+
/** Override the TS signature shown to the model. */
|
|
125
|
+
tsSignature?: string;
|
|
126
|
+
};
|
|
109
127
|
/**
|
|
110
128
|
* Shape of one entry in the host-tool map. Mirrors
|
|
111
129
|
* `@absolutejs/ai/tools`'s `CodeModeHostTool` exactly — no import
|
|
@@ -128,3 +146,30 @@ export type CodeModeHostToolMap = Record<string, CodeModeHostTool>;
|
|
|
128
146
|
* allowlist surfaces at boot, not at the first model call.
|
|
129
147
|
*/
|
|
130
148
|
export declare const engineMutationsAsHostTools: <Ctx>(options: EngineMutationsAsHostToolsOptions<Ctx>) => CodeModeHostToolMap;
|
|
149
|
+
/**
|
|
150
|
+
* A single Code Mode host tool wrapping `engine.runMutations(specs, ctx)`
|
|
151
|
+
* (sync 1.11+). Returns the per-mutation results array; rolls every
|
|
152
|
+
* accumulated write back on any thrown error. Plug the returned
|
|
153
|
+
* `CodeModeHostTool` into a `codeModeTool({ tools: { ..., run_transaction:
|
|
154
|
+
* /* this *\/ } })` map under whatever name fits your prompt strategy.
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* ```ts
|
|
158
|
+
* const hostTools = {
|
|
159
|
+
* ...engineMutationsAsHostTools({ engine, ctx, mutations }),
|
|
160
|
+
* run_transaction: transactionalBatchAsHostTool({
|
|
161
|
+
* engine,
|
|
162
|
+
* ctx,
|
|
163
|
+
* allowedMutations: ['comments:create', 'notifications:notify'],
|
|
164
|
+
* }),
|
|
165
|
+
* };
|
|
166
|
+
* const tool = codeModeTool({ tools: hostTools });
|
|
167
|
+
*
|
|
168
|
+
* // Model can branch on a per-mutation call OR use the atomic batch:
|
|
169
|
+
* // await run_transaction([
|
|
170
|
+
* // { name: 'comments:create', args: { resourceId, body } },
|
|
171
|
+
* // { name: 'notifications:notify', args: { actorId, kind, ... } },
|
|
172
|
+
* // ]);
|
|
173
|
+
* ```
|
|
174
|
+
*/
|
|
175
|
+
export declare const transactionalBatchAsHostTool: <Ctx>(options: TransactionalBatchOptions<Ctx>) => CodeModeHostTool;
|
package/dist/codeMode.js
CHANGED
|
@@ -44,9 +44,41 @@ var engineMutationsAsHostTools = (options) => {
|
|
|
44
44
|
}
|
|
45
45
|
return map;
|
|
46
46
|
};
|
|
47
|
+
var DEFAULT_TRANSACTION_TS_SIGNATURE = "(specs: Array<{ name: string; args: any }>) => Promise<any[]>";
|
|
48
|
+
var DEFAULT_TRANSACTION_DESCRIPTION = "Run an ARRAY of mutations atomically in a single DB transaction. " + "Each entry is `{ name, args }` where `name` is an engine mutation " + "name. If any mutation throws, the entire batch rolls back \u2014 no " + "partial commits. Returns the per-mutation results in order. Use " + "this when you need all-or-nothing semantics; use the individual " + "host functions when you need to branch on intermediate results.";
|
|
49
|
+
var transactionalBatchAsHostTool = (options) => {
|
|
50
|
+
const allowed = new Set(options.allowedMutations);
|
|
51
|
+
return {
|
|
52
|
+
description: options.description ?? DEFAULT_TRANSACTION_DESCRIPTION,
|
|
53
|
+
handler: async (...args) => {
|
|
54
|
+
const specsInput = args[0];
|
|
55
|
+
if (!Array.isArray(specsInput)) {
|
|
56
|
+
throw new Error("transactionalBatchAsHostTool: expected one positional " + "arg \u2014 an array of { name, args }. Got " + typeof specsInput);
|
|
57
|
+
}
|
|
58
|
+
const specs = [];
|
|
59
|
+
for (const entry of specsInput) {
|
|
60
|
+
if (entry === null || typeof entry !== "object" || typeof entry.name !== "string") {
|
|
61
|
+
throw new Error("transactionalBatchAsHostTool: every spec must be " + "`{ name: string; args: any }`.");
|
|
62
|
+
}
|
|
63
|
+
const name = entry.name;
|
|
64
|
+
if (!allowed.has(name)) {
|
|
65
|
+
throw new Error(`transactionalBatchAsHostTool: mutation "${name}" ` + "is not in the allowlist.");
|
|
66
|
+
}
|
|
67
|
+
specs.push({
|
|
68
|
+
args: entry.args,
|
|
69
|
+
name
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const resolvedCtx = await options.ctx();
|
|
73
|
+
return options.engine.runMutations(specs, resolvedCtx);
|
|
74
|
+
},
|
|
75
|
+
tsSignature: options.tsSignature ?? DEFAULT_TRANSACTION_TS_SIGNATURE
|
|
76
|
+
};
|
|
77
|
+
};
|
|
47
78
|
export {
|
|
79
|
+
transactionalBatchAsHostTool,
|
|
48
80
|
engineMutationsAsHostTools
|
|
49
81
|
};
|
|
50
82
|
|
|
51
|
-
//# debugId=
|
|
83
|
+
//# debugId=7E23BE34D770BCA364756E2164756E21
|
|
52
84
|
//# sourceMappingURL=codeMode.js.map
|
package/dist/codeMode.js.map
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/codeMode.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"/**\n * `@absolutejs/sync/code-mode` — wraps engine mutations as Code Mode host\n * tools.\n *\n * The Code Mode pattern (Cloudflare Dynamic Workers / Anthropic\n * programmatic tool calling, both ~2026) replaces \"N tool calls per\n * turn\" with \"1 tool call whose body is JS that chains the underlying\n * fns.\" Sync's contribution: the engine's mutation surface is the\n * underlying fns. The model writes\n *\n * ```js\n * const c = await runMutation('comments:create', { body: '@bob …', resourceId });\n * await runMutation('comments:toggleReaction', { commentId: c.id, emoji: '👍' });\n * return c.id;\n * ```\n *\n * and three would-be tool turns collapse into one — only the final\n * `return` enters the conversation context.\n *\n * The factory here returns a **host-tool map** that's shape-compatible\n * with `@absolutejs/ai`'s `codeModeTool({ tools })` option. We don't\n * import `@absolutejs/ai` because the consumer is responsible for\n * wiring the two; sync stays decoupled from the AI SDK.\n *\n * ## v0.1 semantics (read carefully)\n *\n * - Each `runMutation` call runs in its own DB transaction (the\n * engine's per-call retry/transaction wrapper). If mutation 3/5\n * fails, mutations 1–2 are already committed; the model receives\n * the error and decides whether to compensate (e.g. by calling a\n * delete mutation).\n * - Cross-mutation atomicity (all-or-nothing across N runMutations) is\n * NOT provided here — it would need a new engine batch primitive.\n * That's a deliberate v0.2 followup; this v0.1 ships the integration\n * without lying about transactional semantics.\n * - The host-side `ctx` factory runs ONCE per `run_mutations` tool\n * call; the same ctx is threaded through every mutation in that\n * script. This matches the \"one model turn ≈ one user request\"\n * shape, where the user identity doesn't change mid-script.\n *\n * @example\n * ```ts\n * import { engineMutationsAsHostTools } from '@absolutejs/sync/code-mode';\n * import { codeModeTool } from '@absolutejs/ai/tools';\n *\n * const sessionCtx = () => ({ userId: currentSessionUserId() });\n * const hostTools = engineMutationsAsHostTools({\n * ctx: sessionCtx,\n * engine,\n * mutations: [\n * {\n * description: 'Post a comment on a resource.',\n * name: 'comments:create',\n * tsSignature:\n * '(args: { resourceId: string; body: string }) => ' +\n * 'Promise<{ id: string; authorId: string; body: string }>',\n * },\n * {\n * description: 'Toggle a reaction emoji on a comment.',\n * name: 'comments:toggleReaction',\n * tsSignature:\n * '(args: { commentId: string; emoji: string }) => ' +\n * 'Promise<{ added: boolean }>',\n * },\n * ],\n * });\n *\n * const aiTool = codeModeTool({ tools: hostTools });\n * ```\n */\n\nimport type { SyncEngine } from './engine/syncEngine';\n\n/** One engine mutation surfaced to the model as a host function. */\nexport type MutationToolDescriptor = {\n\t/** Engine-registered mutation name (e.g. `'comments:create'`). */\n\tname: string;\n\t/**\n\t * One-line description shown to the model in the Code Mode prompt.\n\t * Should describe *what the mutation does*, not just its inputs.\n\t */\n\tdescription: string;\n\t/**\n\t * TypeScript signature shown to the model. Default\n\t * `'(args: any) => Promise<any>'` — provide a real signature so the\n\t * model knows the input shape.\n\t */\n\ttsSignature?: string;\n\t/**\n\t * Override the host-fn name surfaced to the model. Default: the\n\t * mutation `name` with `:` replaced by `_` (engine names are not\n\t * valid JS identifiers). For `'comments:create'` the model sees\n\t * `comments_create` by default.\n\t */\n\thostFnName?: string;\n};\n\n/** Options for {@link engineMutationsAsHostTools}. */\nexport type EngineMutationsAsHostToolsOptions<Ctx> = {\n\t/** The engine whose mutations should be exposed. */\n\tengine: SyncEngine;\n\t/**\n\t * Resolve the per-call ctx — invoked ONCE at the start of each\n\t * Code Mode `run_mutations` call and threaded through every\n\t * mutation in that script. Typically returns the current\n\t * session/user identity. May be sync or async.\n\t */\n\tctx: () => Ctx | Promise<Ctx>;\n\t/** The mutation set to expose. */\n\tmutations: MutationToolDescriptor[];\n};\n\n/**\n * Shape of one entry in the host-tool map. Mirrors\n * `@absolutejs/ai/tools`'s `CodeModeHostTool` exactly — no import\n * needed; the AI SDK consumes any record with this shape.\n */\nexport type CodeModeHostTool = {\n\tdescription: string;\n\ttsSignature: string;\n\thandler: (...args: unknown[]) => unknown;\n};\n\n/** Map of model-visible host-fn name → host tool. */\nexport type CodeModeHostToolMap = Record<string, CodeModeHostTool>;\n\nconst DEFAULT_TS_SIGNATURE = '(args: any) => Promise<any>';\n\nconst defaultHostFnName = (mutationName: string): string =>\n\tmutationName.replace(/[^A-Za-z0-9_]/g, '_');\n\n/**\n * Build a host-tool map that exposes the engine's mutation surface to a\n * Code Mode tool. Pass the result to `codeModeTool({ tools })` from\n * `@absolutejs/ai/tools`.\n *\n * The function throws synchronously at build time if any descriptor\n * names a mutation that isn't registered on the engine, so a typo'd\n * allowlist surfaces at boot, not at the first model call.\n */\nexport const engineMutationsAsHostTools = <Ctx>(\n\toptions: EngineMutationsAsHostToolsOptions<Ctx>\n): CodeModeHostToolMap => {\n\tconst { engine, ctx, mutations } = options;\n\tconst registered = new Set(engine.inspect().mutations);\n\tconst map: CodeModeHostToolMap = {};\n\tconst seenHostFnNames = new Set<string>();\n\n\tfor (const descriptor of mutations) {\n\t\tif (!registered.has(descriptor.name)) {\n\t\t\tthrow new Error(\n\t\t\t\t`engineMutationsAsHostTools: mutation \"${descriptor.name}\" is not registered on the engine. ` +\n\t\t\t\t\t`Register it first, or remove it from the mutations list.`\n\t\t\t);\n\t\t}\n\t\tconst hostFnName =\n\t\t\tdescriptor.hostFnName ?? defaultHostFnName(descriptor.name);\n\t\tif (seenHostFnNames.has(hostFnName)) {\n\t\t\tthrow new Error(\n\t\t\t\t`engineMutationsAsHostTools: duplicate host-fn name \"${hostFnName}\". ` +\n\t\t\t\t\t`Two mutations map to the same identifier — set hostFnName explicitly on one of them.`\n\t\t\t);\n\t\t}\n\t\tseenHostFnNames.add(hostFnName);\n\n\t\tmap[hostFnName] = {\n\t\t\tdescription: descriptor.description,\n\t\t\thandler: async (...args: unknown[]): Promise<unknown> => {\n\t\t\t\t// Code Mode passes positional args from the model's JS call.\n\t\t\t\t// Engine mutations take a single `args` value — first\n\t\t\t\t// positional arg is the mutation payload.\n\t\t\t\tconst payload = args[0];\n\t\t\t\tconst resolvedCtx = await ctx();\n\t\t\t\treturn engine.runMutation(\n\t\t\t\t\tdescriptor.name,\n\t\t\t\t\tpayload,\n\t\t\t\t\tresolvedCtx\n\t\t\t\t);\n\t\t\t},\n\t\t\ttsSignature: descriptor.tsSignature ?? DEFAULT_TS_SIGNATURE\n\t\t};\n\t}\n\n\treturn map;\n};\n"
|
|
5
|
+
"/**\n * `@absolutejs/sync/code-mode` — wraps engine mutations as Code Mode host\n * tools.\n *\n * The Code Mode pattern (Cloudflare Dynamic Workers / Anthropic\n * programmatic tool calling, both ~2026) replaces \"N tool calls per\n * turn\" with \"1 tool call whose body is JS that chains the underlying\n * fns.\" Sync's contribution: the engine's mutation surface is the\n * underlying fns. The model writes\n *\n * ```js\n * const c = await runMutation('comments:create', { body: '@bob …', resourceId });\n * await runMutation('comments:toggleReaction', { commentId: c.id, emoji: '👍' });\n * return c.id;\n * ```\n *\n * and three would-be tool turns collapse into one — only the final\n * `return` enters the conversation context.\n *\n * The factory here returns a **host-tool map** that's shape-compatible\n * with `@absolutejs/ai`'s `codeModeTool({ tools })` option. We don't\n * import `@absolutejs/ai` because the consumer is responsible for\n * wiring the two; sync stays decoupled from the AI SDK.\n *\n * ## v0.1 semantics (read carefully)\n *\n * - Each `runMutation` call runs in its own DB transaction (the\n * engine's per-call retry/transaction wrapper). If mutation 3/5\n * fails, mutations 1–2 are already committed; the model receives\n * the error and decides whether to compensate (e.g. by calling a\n * delete mutation).\n * - Cross-mutation atomicity (all-or-nothing across N runMutations) is\n * NOT provided here — it would need a new engine batch primitive.\n * That's a deliberate v0.2 followup; this v0.1 ships the integration\n * without lying about transactional semantics.\n * - The host-side `ctx` factory runs ONCE per `run_mutations` tool\n * call; the same ctx is threaded through every mutation in that\n * script. This matches the \"one model turn ≈ one user request\"\n * shape, where the user identity doesn't change mid-script.\n *\n * @example\n * ```ts\n * import { engineMutationsAsHostTools } from '@absolutejs/sync/code-mode';\n * import { codeModeTool } from '@absolutejs/ai/tools';\n *\n * const sessionCtx = () => ({ userId: currentSessionUserId() });\n * const hostTools = engineMutationsAsHostTools({\n * ctx: sessionCtx,\n * engine,\n * mutations: [\n * {\n * description: 'Post a comment on a resource.',\n * name: 'comments:create',\n * tsSignature:\n * '(args: { resourceId: string; body: string }) => ' +\n * 'Promise<{ id: string; authorId: string; body: string }>',\n * },\n * {\n * description: 'Toggle a reaction emoji on a comment.',\n * name: 'comments:toggleReaction',\n * tsSignature:\n * '(args: { commentId: string; emoji: string }) => ' +\n * 'Promise<{ added: boolean }>',\n * },\n * ],\n * });\n *\n * const aiTool = codeModeTool({ tools: hostTools });\n * ```\n */\n\nimport type { SyncEngine } from './engine/syncEngine';\n\n/** One engine mutation surfaced to the model as a host function. */\nexport type MutationToolDescriptor = {\n\t/** Engine-registered mutation name (e.g. `'comments:create'`). */\n\tname: string;\n\t/**\n\t * One-line description shown to the model in the Code Mode prompt.\n\t * Should describe *what the mutation does*, not just its inputs.\n\t */\n\tdescription: string;\n\t/**\n\t * TypeScript signature shown to the model. Default\n\t * `'(args: any) => Promise<any>'` — provide a real signature so the\n\t * model knows the input shape.\n\t */\n\ttsSignature?: string;\n\t/**\n\t * Override the host-fn name surfaced to the model. Default: the\n\t * mutation `name` with `:` replaced by `_` (engine names are not\n\t * valid JS identifiers). For `'comments:create'` the model sees\n\t * `comments_create` by default.\n\t */\n\thostFnName?: string;\n};\n\n/** Options for {@link engineMutationsAsHostTools}. */\nexport type EngineMutationsAsHostToolsOptions<Ctx> = {\n\t/** The engine whose mutations should be exposed. */\n\tengine: SyncEngine;\n\t/**\n\t * Resolve the per-call ctx — invoked ONCE at the start of each\n\t * Code Mode `run_mutations` call and threaded through every\n\t * mutation in that script. Typically returns the current\n\t * session/user identity. May be sync or async.\n\t */\n\tctx: () => Ctx | Promise<Ctx>;\n\t/** The mutation set to expose. */\n\tmutations: MutationToolDescriptor[];\n};\n\n/** Options for {@link transactionalBatchAsHostTool}. */\nexport type TransactionalBatchOptions<Ctx> = {\n\t/** The engine to call `runMutations` on. */\n\tengine: SyncEngine;\n\t/** Resolve per-call ctx — same shape as the other factory. */\n\tctx: () => Ctx | Promise<Ctx>;\n\t/**\n\t * Allowlist of mutation names the model may include in a batch.\n\t * The host-fn checks every entry against this set before calling\n\t * `runMutations`, so a hallucinated name fails fast with a clear\n\t * error instead of bubbling up from the engine.\n\t */\n\tallowedMutations: string[];\n\t/** Override the model-visible host-fn description. */\n\tdescription?: string;\n\t/** Override the TS signature shown to the model. */\n\ttsSignature?: string;\n};\n\n/**\n * Shape of one entry in the host-tool map. Mirrors\n * `@absolutejs/ai/tools`'s `CodeModeHostTool` exactly — no import\n * needed; the AI SDK consumes any record with this shape.\n */\nexport type CodeModeHostTool = {\n\tdescription: string;\n\ttsSignature: string;\n\thandler: (...args: unknown[]) => unknown;\n};\n\n/** Map of model-visible host-fn name → host tool. */\nexport type CodeModeHostToolMap = Record<string, CodeModeHostTool>;\n\nconst DEFAULT_TS_SIGNATURE = '(args: any) => Promise<any>';\n\nconst defaultHostFnName = (mutationName: string): string =>\n\tmutationName.replace(/[^A-Za-z0-9_]/g, '_');\n\n/**\n * Build a host-tool map that exposes the engine's mutation surface to a\n * Code Mode tool. Pass the result to `codeModeTool({ tools })` from\n * `@absolutejs/ai/tools`.\n *\n * The function throws synchronously at build time if any descriptor\n * names a mutation that isn't registered on the engine, so a typo'd\n * allowlist surfaces at boot, not at the first model call.\n */\nexport const engineMutationsAsHostTools = <Ctx>(\n\toptions: EngineMutationsAsHostToolsOptions<Ctx>\n): CodeModeHostToolMap => {\n\tconst { engine, ctx, mutations } = options;\n\tconst registered = new Set(engine.inspect().mutations);\n\tconst map: CodeModeHostToolMap = {};\n\tconst seenHostFnNames = new Set<string>();\n\n\tfor (const descriptor of mutations) {\n\t\tif (!registered.has(descriptor.name)) {\n\t\t\tthrow new Error(\n\t\t\t\t`engineMutationsAsHostTools: mutation \"${descriptor.name}\" is not registered on the engine. ` +\n\t\t\t\t\t`Register it first, or remove it from the mutations list.`\n\t\t\t);\n\t\t}\n\t\tconst hostFnName =\n\t\t\tdescriptor.hostFnName ?? defaultHostFnName(descriptor.name);\n\t\tif (seenHostFnNames.has(hostFnName)) {\n\t\t\tthrow new Error(\n\t\t\t\t`engineMutationsAsHostTools: duplicate host-fn name \"${hostFnName}\". ` +\n\t\t\t\t\t`Two mutations map to the same identifier — set hostFnName explicitly on one of them.`\n\t\t\t);\n\t\t}\n\t\tseenHostFnNames.add(hostFnName);\n\n\t\tmap[hostFnName] = {\n\t\t\tdescription: descriptor.description,\n\t\t\thandler: async (...args: unknown[]): Promise<unknown> => {\n\t\t\t\t// Code Mode passes positional args from the model's JS call.\n\t\t\t\t// Engine mutations take a single `args` value — first\n\t\t\t\t// positional arg is the mutation payload.\n\t\t\t\tconst payload = args[0];\n\t\t\t\tconst resolvedCtx = await ctx();\n\t\t\t\treturn engine.runMutation(\n\t\t\t\t\tdescriptor.name,\n\t\t\t\t\tpayload,\n\t\t\t\t\tresolvedCtx\n\t\t\t\t);\n\t\t\t},\n\t\t\ttsSignature: descriptor.tsSignature ?? DEFAULT_TS_SIGNATURE\n\t\t};\n\t}\n\n\treturn map;\n};\n\nconst DEFAULT_TRANSACTION_TS_SIGNATURE =\n\t'(specs: Array<{ name: string; args: any }>) => Promise<any[]>';\n\nconst DEFAULT_TRANSACTION_DESCRIPTION =\n\t'Run an ARRAY of mutations atomically in a single DB transaction. ' +\n\t'Each entry is `{ name, args }` where `name` is an engine mutation ' +\n\t'name. If any mutation throws, the entire batch rolls back — no ' +\n\t'partial commits. Returns the per-mutation results in order. Use ' +\n\t'this when you need all-or-nothing semantics; use the individual ' +\n\t'host functions when you need to branch on intermediate results.';\n\n/**\n * A single Code Mode host tool wrapping `engine.runMutations(specs, ctx)`\n * (sync 1.11+). Returns the per-mutation results array; rolls every\n * accumulated write back on any thrown error. Plug the returned\n * `CodeModeHostTool` into a `codeModeTool({ tools: { ..., run_transaction:\n * /* this *\\/ } })` map under whatever name fits your prompt strategy.\n *\n * @example\n * ```ts\n * const hostTools = {\n * ...engineMutationsAsHostTools({ engine, ctx, mutations }),\n * run_transaction: transactionalBatchAsHostTool({\n * engine,\n * ctx,\n * allowedMutations: ['comments:create', 'notifications:notify'],\n * }),\n * };\n * const tool = codeModeTool({ tools: hostTools });\n *\n * // Model can branch on a per-mutation call OR use the atomic batch:\n * // await run_transaction([\n * // { name: 'comments:create', args: { resourceId, body } },\n * // { name: 'notifications:notify', args: { actorId, kind, ... } },\n * // ]);\n * ```\n */\nexport const transactionalBatchAsHostTool = <Ctx>(\n\toptions: TransactionalBatchOptions<Ctx>\n): CodeModeHostTool => {\n\tconst allowed = new Set(options.allowedMutations);\n\treturn {\n\t\tdescription: options.description ?? DEFAULT_TRANSACTION_DESCRIPTION,\n\t\thandler: async (...args: unknown[]): Promise<unknown> => {\n\t\t\t// The model writes `await run_transaction([...])` — Code Mode\n\t\t\t// passes that array as the first positional arg.\n\t\t\tconst specsInput = args[0];\n\t\t\tif (!Array.isArray(specsInput)) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t'transactionalBatchAsHostTool: expected one positional ' +\n\t\t\t\t\t\t'arg — an array of { name, args }. Got ' +\n\t\t\t\t\t\ttypeof specsInput\n\t\t\t\t);\n\t\t\t}\n\t\t\tconst specs: Array<{ name: string; args: unknown }> = [];\n\t\t\tfor (const entry of specsInput as unknown[]) {\n\t\t\t\tif (\n\t\t\t\t\tentry === null ||\n\t\t\t\t\ttypeof entry !== 'object' ||\n\t\t\t\t\ttypeof (entry as { name?: unknown }).name !== 'string'\n\t\t\t\t) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t'transactionalBatchAsHostTool: every spec must be ' +\n\t\t\t\t\t\t\t'`{ name: string; args: any }`.'\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tconst name = (entry as { name: string }).name;\n\t\t\t\tif (!allowed.has(name)) {\n\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t`transactionalBatchAsHostTool: mutation \"${name}\" ` +\n\t\t\t\t\t\t\t'is not in the allowlist.'\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t\tspecs.push({\n\t\t\t\t\targs: (entry as { args?: unknown }).args,\n\t\t\t\t\tname\n\t\t\t\t});\n\t\t\t}\n\t\t\tconst resolvedCtx = await options.ctx();\n\t\t\treturn options.engine.runMutations(specs, resolvedCtx);\n\t\t},\n\t\ttsSignature: options.tsSignature ?? DEFAULT_TRANSACTION_TS_SIGNATURE\n\t};\n};\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": ";;;;;;;;;;;;;;;;;;
|
|
8
|
-
"debugId": "
|
|
7
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAiJA,IAAM,uBAAuB;AAE7B,IAAM,oBAAoB,CAAC,iBAC1B,aAAa,QAAQ,kBAAkB,GAAG;AAWpC,IAAM,6BAA6B,CACzC,YACyB;AAAA,EACzB,QAAQ,QAAQ,KAAK,cAAc;AAAA,EACnC,MAAM,aAAa,IAAI,IAAI,OAAO,QAAQ,EAAE,SAAS;AAAA,EACrD,MAAM,MAA2B,CAAC;AAAA,EAClC,MAAM,kBAAkB,IAAI;AAAA,EAE5B,WAAW,cAAc,WAAW;AAAA,IACnC,IAAI,CAAC,WAAW,IAAI,WAAW,IAAI,GAAG;AAAA,MACrC,MAAM,IAAI,MACT,yCAAyC,WAAW,4CACnD,0DACF;AAAA,IACD;AAAA,IACA,MAAM,aACL,WAAW,cAAc,kBAAkB,WAAW,IAAI;AAAA,IAC3D,IAAI,gBAAgB,IAAI,UAAU,GAAG;AAAA,MACpC,MAAM,IAAI,MACT,uDAAuD,kBACtD,2FACF;AAAA,IACD;AAAA,IACA,gBAAgB,IAAI,UAAU;AAAA,IAE9B,IAAI,cAAc;AAAA,MACjB,aAAa,WAAW;AAAA,MACxB,SAAS,UAAU,SAAsC;AAAA,QAIxD,MAAM,UAAU,KAAK;AAAA,QACrB,MAAM,cAAc,MAAM,IAAI;AAAA,QAC9B,OAAO,OAAO,YACb,WAAW,MACX,SACA,WACD;AAAA;AAAA,MAED,aAAa,WAAW,eAAe;AAAA,IACxC;AAAA,EACD;AAAA,EAEA,OAAO;AAAA;AAGR,IAAM,mCACL;AAED,IAAM,kCACL,sEACA,uEACA,yEACA,qEACA,qEACA;AA4BM,IAAM,+BAA+B,CAC3C,YACsB;AAAA,EACtB,MAAM,UAAU,IAAI,IAAI,QAAQ,gBAAgB;AAAA,EAChD,OAAO;AAAA,IACN,aAAa,QAAQ,eAAe;AAAA,IACpC,SAAS,UAAU,SAAsC;AAAA,MAGxD,MAAM,aAAa,KAAK;AAAA,MACxB,IAAI,CAAC,MAAM,QAAQ,UAAU,GAAG;AAAA,QAC/B,MAAM,IAAI,MACT,2DACC,gDACA,OAAO,UACT;AAAA,MACD;AAAA,MACA,MAAM,QAAgD,CAAC;AAAA,MACvD,WAAW,SAAS,YAAyB;AAAA,QAC5C,IACC,UAAU,QACV,OAAO,UAAU,YACjB,OAAQ,MAA6B,SAAS,UAC7C;AAAA,UACD,MAAM,IAAI,MACT,sDACC,gCACF;AAAA,QACD;AAAA,QACA,MAAM,OAAQ,MAA2B;AAAA,QACzC,IAAI,CAAC,QAAQ,IAAI,IAAI,GAAG;AAAA,UACvB,MAAM,IAAI,MACT,2CAA2C,WAC1C,0BACF;AAAA,QACD;AAAA,QACA,MAAM,KAAK;AAAA,UACV,MAAO,MAA6B;AAAA,UACpC;AAAA,QACD,CAAC;AAAA,MACF;AAAA,MACA,MAAM,cAAc,MAAM,QAAQ,IAAI;AAAA,MACtC,OAAO,QAAQ,OAAO,aAAa,OAAO,WAAW;AAAA;AAAA,IAEtD,aAAa,QAAQ,eAAe;AAAA,EACrC;AAAA;",
|
|
8
|
+
"debugId": "7E23BE34D770BCA364756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -64,6 +64,15 @@ export type EngineActivity = {
|
|
|
64
64
|
at: number;
|
|
65
65
|
name: string;
|
|
66
66
|
status: 'ok' | 'error';
|
|
67
|
+
} | {
|
|
68
|
+
/** Emitted by `engine.runMutations(...)` — a batch of mutations
|
|
69
|
+
* run in a single transaction. `names` is the list in batch
|
|
70
|
+
* order; on error the entire batch rolls back, so the status
|
|
71
|
+
* applies to the whole list, not any individual mutation. */
|
|
72
|
+
type: 'mutationBatch';
|
|
73
|
+
at: number;
|
|
74
|
+
names: string[];
|
|
75
|
+
status: 'ok' | 'error';
|
|
67
76
|
} | {
|
|
68
77
|
/** Emitted between attempts of a retried mutation. `attempt` is the
|
|
69
78
|
* attempt that just failed (1-indexed); `delayMs` is the wait before
|
package/dist/engine/index.js
CHANGED
|
@@ -1136,7 +1136,7 @@ var wrap = (source) => `
|
|
|
1136
1136
|
const userFn = (${source});
|
|
1137
1137
|
if (typeof userFn !== 'function') {
|
|
1138
1138
|
throw new Error(
|
|
1139
|
-
'sandboxedHandler must evaluate to (args, ctx, actions) => result; got ' +
|
|
1139
|
+
'sandboxedHandler must evaluate to (args, ctx, actions, unsafeHost) => result; got ' +
|
|
1140
1140
|
typeof userFn
|
|
1141
1141
|
);
|
|
1142
1142
|
}
|
|
@@ -1148,12 +1148,24 @@ var wrap = (source) => `
|
|
|
1148
1148
|
now: () => __dispatch(__callId, 'now'),
|
|
1149
1149
|
fetch: (url, init) => __dispatch(__callId, 'fetch', url, init)
|
|
1150
1150
|
};
|
|
1151
|
-
|
|
1151
|
+
// Escape hatch \u2014 host fns the mutation explicitly opted in to.
|
|
1152
|
+
// The Proxy means every property access is a host call; the
|
|
1153
|
+
// engine throws if the property name isn't declared in the
|
|
1154
|
+
// mutation's sandbox.unsafeHost map.
|
|
1155
|
+
const unsafeHost = new Proxy({}, {
|
|
1156
|
+
get: (_target, fnName) => {
|
|
1157
|
+
if (typeof fnName !== 'string') return undefined;
|
|
1158
|
+
return (...callArgs) =>
|
|
1159
|
+
__dispatch(__callId, 'unsafeHost', fnName, callArgs);
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
return userFn(args, ctx, actions, unsafeHost);
|
|
1152
1163
|
}
|
|
1153
1164
|
`;
|
|
1154
1165
|
var compile = async (source, config, bridgeFetch) => {
|
|
1155
1166
|
const { Reference, createIsolatedRunner, resolveIsolatePolicy } = await loadIsolatedJsc();
|
|
1156
1167
|
const callMap = new Map;
|
|
1168
|
+
const unsafeHost = config.unsafeHost;
|
|
1157
1169
|
const dispatch = new Reference((callId, op, ...rest) => {
|
|
1158
1170
|
const a = callMap.get(callId);
|
|
1159
1171
|
if (a === undefined) {
|
|
@@ -1172,6 +1184,14 @@ var compile = async (source, config, bridgeFetch) => {
|
|
|
1172
1184
|
return a.now();
|
|
1173
1185
|
case "fetch":
|
|
1174
1186
|
return runBridgeFetch(bridgeFetch, rest[0], rest[1]);
|
|
1187
|
+
case "unsafeHost": {
|
|
1188
|
+
const fnName = rest[0];
|
|
1189
|
+
const callArgs = rest[1] ?? [];
|
|
1190
|
+
if (unsafeHost === undefined || typeof unsafeHost[fnName] !== "function") {
|
|
1191
|
+
throw new Error(`sandboxedHandler called unsafeHost.${fnName}() but it was not declared in the mutation's sandbox.unsafeHost config. Declare it (and only the host fns you intend to expose) to opt in to the escape hatch.`);
|
|
1192
|
+
}
|
|
1193
|
+
return unsafeHost[fnName](...callArgs);
|
|
1194
|
+
}
|
|
1175
1195
|
default:
|
|
1176
1196
|
throw new Error(`unknown sandbox action op: ${String(op)}`);
|
|
1177
1197
|
}
|
|
@@ -2428,6 +2448,55 @@ var createSyncEngine = (options = {}) => {
|
|
|
2428
2448
|
}
|
|
2429
2449
|
throw lastError;
|
|
2430
2450
|
},
|
|
2451
|
+
runMutations: async (specs, ctx) => {
|
|
2452
|
+
if (specs.length === 0)
|
|
2453
|
+
return [];
|
|
2454
|
+
const resolved = specs.map((spec) => {
|
|
2455
|
+
const mutation = mutations.get(spec.name);
|
|
2456
|
+
if (mutation === undefined) {
|
|
2457
|
+
throw new Error(`Unknown mutation "${spec.name}"`);
|
|
2458
|
+
}
|
|
2459
|
+
return { args: spec.args, mutation, name: spec.name };
|
|
2460
|
+
});
|
|
2461
|
+
const runBatch = async (tx) => {
|
|
2462
|
+
const results = [];
|
|
2463
|
+
const accumulated = [];
|
|
2464
|
+
for (const { args, mutation, name } of resolved) {
|
|
2465
|
+
if (mutation.authorize !== undefined) {
|
|
2466
|
+
const allowed = await mutation.authorize(args, ctx);
|
|
2467
|
+
if (!allowed) {
|
|
2468
|
+
throw new UnauthorizedError(`run mutation "${name}"`);
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
const sandboxRunner = sandboxRunners.get(name);
|
|
2472
|
+
const invokeHandler = sandboxRunner !== undefined ? sandboxRunner : (a, c, actions2) => Promise.resolve(mutation.handler(a, c, actions2));
|
|
2473
|
+
const { actions, buffered } = makeActions(tx, ctx, true);
|
|
2474
|
+
const result = await invokeHandler(args, ctx, actions);
|
|
2475
|
+
results.push(result);
|
|
2476
|
+
accumulated.push(...buffered);
|
|
2477
|
+
}
|
|
2478
|
+
return { accumulated, results };
|
|
2479
|
+
};
|
|
2480
|
+
try {
|
|
2481
|
+
const { accumulated, results } = runInTransaction !== undefined ? await runInTransaction((tx) => runBatch(tx)) : await runBatch(undefined);
|
|
2482
|
+
await applyChangeBatch(accumulated);
|
|
2483
|
+
emitActivity({
|
|
2484
|
+
type: "mutationBatch",
|
|
2485
|
+
at: Date.now(),
|
|
2486
|
+
names: resolved.map((entry) => entry.name),
|
|
2487
|
+
status: "ok"
|
|
2488
|
+
});
|
|
2489
|
+
return results;
|
|
2490
|
+
} catch (error) {
|
|
2491
|
+
emitActivity({
|
|
2492
|
+
type: "mutationBatch",
|
|
2493
|
+
at: Date.now(),
|
|
2494
|
+
names: resolved.map((entry) => entry.name),
|
|
2495
|
+
status: "error"
|
|
2496
|
+
});
|
|
2497
|
+
throw error;
|
|
2498
|
+
}
|
|
2499
|
+
},
|
|
2431
2500
|
registerSchedule: (schedule) => {
|
|
2432
2501
|
schedules.set(schedule.name, schedule);
|
|
2433
2502
|
},
|
|
@@ -3091,5 +3160,5 @@ export {
|
|
|
3091
3160
|
CdcConsumerSlowError
|
|
3092
3161
|
};
|
|
3093
3162
|
|
|
3094
|
-
//# debugId=
|
|
3163
|
+
//# debugId=5C3B639DC6FB356064756E2164756E21
|
|
3095
3164
|
//# sourceMappingURL=index.js.map
|