@grimoirelabs/core 0.7.0 → 0.9.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.
Files changed (79) hide show
  1. package/dist/compiler/grimoire/ast.d.ts +5 -0
  2. package/dist/compiler/grimoire/ast.d.ts.map +1 -1
  3. package/dist/compiler/grimoire/ast.js.map +1 -1
  4. package/dist/compiler/grimoire/errors.d.ts +1 -1
  5. package/dist/compiler/grimoire/errors.d.ts.map +1 -1
  6. package/dist/compiler/grimoire/errors.js +2 -2
  7. package/dist/compiler/grimoire/errors.js.map +1 -1
  8. package/dist/compiler/grimoire/index.d.ts.map +1 -1
  9. package/dist/compiler/grimoire/index.js +8 -1
  10. package/dist/compiler/grimoire/index.js.map +1 -1
  11. package/dist/compiler/grimoire/parser.d.ts +3 -0
  12. package/dist/compiler/grimoire/parser.d.ts.map +1 -1
  13. package/dist/compiler/grimoire/parser.js +106 -25
  14. package/dist/compiler/grimoire/parser.js.map +1 -1
  15. package/dist/compiler/grimoire/transformer.d.ts +0 -1
  16. package/dist/compiler/grimoire/transformer.d.ts.map +1 -1
  17. package/dist/compiler/grimoire/transformer.js +57 -100
  18. package/dist/compiler/grimoire/transformer.js.map +1 -1
  19. package/dist/compiler/index.d.ts +1 -1
  20. package/dist/compiler/index.d.ts.map +1 -1
  21. package/dist/compiler/index.js +5 -1
  22. package/dist/compiler/index.js.map +1 -1
  23. package/dist/compiler/ir-generator.js +33 -0
  24. package/dist/compiler/ir-generator.js.map +1 -1
  25. package/dist/compiler/type-checker.d.ts +2 -2
  26. package/dist/compiler/type-checker.js +2 -2
  27. package/dist/compiler/validator.d.ts +8 -0
  28. package/dist/compiler/validator.d.ts.map +1 -1
  29. package/dist/compiler/validator.js +223 -0
  30. package/dist/compiler/validator.js.map +1 -1
  31. package/dist/index.d.ts +3 -3
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +1 -1
  34. package/dist/index.js.map +1 -1
  35. package/dist/runtime/context.d.ts +3 -1
  36. package/dist/runtime/context.d.ts.map +1 -1
  37. package/dist/runtime/context.js +26 -5
  38. package/dist/runtime/context.js.map +1 -1
  39. package/dist/runtime/index.d.ts +3 -1
  40. package/dist/runtime/index.d.ts.map +1 -1
  41. package/dist/runtime/index.js +3 -1
  42. package/dist/runtime/index.js.map +1 -1
  43. package/dist/runtime/interpreter.d.ts +53 -3
  44. package/dist/runtime/interpreter.d.ts.map +1 -1
  45. package/dist/runtime/interpreter.js +777 -132
  46. package/dist/runtime/interpreter.js.map +1 -1
  47. package/dist/runtime/session-views.d.ts +37 -0
  48. package/dist/runtime/session-views.d.ts.map +1 -0
  49. package/dist/runtime/session-views.js +97 -0
  50. package/dist/runtime/session-views.js.map +1 -0
  51. package/dist/runtime/session.d.ts +35 -0
  52. package/dist/runtime/session.d.ts.map +1 -0
  53. package/dist/runtime/session.js +57 -0
  54. package/dist/runtime/session.js.map +1 -0
  55. package/dist/runtime/steps/action.d.ts +22 -0
  56. package/dist/runtime/steps/action.d.ts.map +1 -1
  57. package/dist/runtime/steps/action.js +209 -0
  58. package/dist/runtime/steps/action.js.map +1 -1
  59. package/dist/runtime/steps/advisory.d.ts +1 -0
  60. package/dist/runtime/steps/advisory.d.ts.map +1 -1
  61. package/dist/runtime/steps/advisory.js +277 -11
  62. package/dist/runtime/steps/advisory.js.map +1 -1
  63. package/dist/runtime/value-flow.d.ts +27 -0
  64. package/dist/runtime/value-flow.d.ts.map +1 -0
  65. package/dist/runtime/value-flow.js +566 -0
  66. package/dist/runtime/value-flow.js.map +1 -0
  67. package/dist/types/execution.d.ts +67 -1
  68. package/dist/types/execution.d.ts.map +1 -1
  69. package/dist/types/index.d.ts +2 -1
  70. package/dist/types/index.d.ts.map +1 -1
  71. package/dist/types/receipt.d.ts +193 -0
  72. package/dist/types/receipt.d.ts.map +1 -0
  73. package/dist/types/receipt.js +5 -0
  74. package/dist/types/receipt.js.map +1 -0
  75. package/dist/types/steps.d.ts +6 -0
  76. package/dist/types/steps.d.ts.map +1 -1
  77. package/dist/wallet/tx-builder.js +3 -3
  78. package/dist/wallet/tx-builder.js.map +1 -1
  79. package/package.json +1 -1
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Spell Interpreter
3
- * Executes compiled SpellIR
3
+ * Executes compiled SpellIR via the preview/commit model.
4
4
  */
5
5
  import { createExecutor } from "../wallet/executor.js";
6
6
  import { createProvider } from "../wallet/provider.js";
@@ -8,8 +8,9 @@ import { CircuitBreakerManager } from "./circuit-breaker.js";
8
8
  import { InMemoryLedger, createContext, getPersistentStateObject, incrementAdvisoryCalls, markStepExecuted, } from "./context.js";
9
9
  import { createEvalContext, evaluateAsync } from "./expression-evaluator.js";
10
10
  import { resolveAdvisorSkill } from "./skills/registry.js";
11
+ import { evaluatePreviewValueFlow, inferDriftClass, } from "./value-flow.js";
11
12
  // Step executors
12
- import { executeActionStep } from "./steps/action.js";
13
+ import { commitActionStep, executeActionStep, previewActionStep, } from "./steps/action.js";
13
14
  import { executeAdvisoryStep } from "./steps/advisory.js";
14
15
  import { executeComputeStep } from "./steps/compute.js";
15
16
  import { executeConditionalStep } from "./steps/conditional.js";
@@ -20,29 +21,36 @@ import { executeParallelStep } from "./steps/parallel.js";
20
21
  import { executePipelineStep } from "./steps/pipeline.js";
21
22
  import { executeTryStep } from "./steps/try.js";
22
23
  import { executeWaitStep } from "./steps/wait.js";
24
+ // Keep preview-issued receipts in-process so commit only accepts known receipts.
25
+ const issuedReceipts = new Map();
26
+ const committedReceipts = new Set();
23
27
  /**
24
- * Execute a compiled spell
28
+ * Preview a spell — runs the full step loop in simulation mode,
29
+ * collects PlannedActions and ValueDeltas, and assembles a Receipt.
25
30
  */
26
- export async function execute(options) {
27
- const { spell, vault, chain, params = {}, persistentState = {}, simulate = false } = options;
28
- const actionMode = resolveExecutionMode(options, simulate);
29
- const actionExecution = createActionExecutionOptions(options, actionMode, chain);
30
- // Initialize circuit breaker manager if policy has breakers
31
+ export async function preview(options) {
32
+ const { spell, vault, chain, params = {}, persistentState = {} } = options;
33
+ // Always simulate during preview
34
+ const actionExecution = { mode: "simulate" };
31
35
  if (options.policy?.circuitBreakers?.length) {
32
36
  actionExecution.circuitBreakerManager = new CircuitBreakerManager(options.policy.circuitBreakers);
33
37
  }
34
- // Create execution context
35
38
  const ctx = createContext({
36
39
  spell,
37
40
  vault,
38
41
  chain,
42
+ trigger: options.trigger,
39
43
  params,
40
44
  persistentState,
41
45
  });
42
46
  ctx.advisorTooling = buildAdvisorTooling(spell.advisors, options.advisorSkillsDirs);
43
- // Create ledger
44
- const ledger = new InMemoryLedger(ctx.runId, spell.id);
45
- // Log run start
47
+ const ledger = new InMemoryLedger(ctx.runId, spell.id, options.eventCallback);
48
+ const guardResults = [];
49
+ const advisoryResults = [];
50
+ const plannedActions = [];
51
+ const valueDeltas = [];
52
+ const receiptId = `rcpt_${ctx.runId}`;
53
+ ledger.emit({ type: "preview_started", runId: ctx.runId, spellId: spell.id });
46
54
  ledger.emit({
47
55
  type: "run_started",
48
56
  runId: ctx.runId,
@@ -50,126 +58,631 @@ export async function execute(options) {
50
58
  trigger: ctx.trigger,
51
59
  });
52
60
  try {
53
- // Check pre-execution guards
54
- const guardResult = await checkGuards(spell.guards, ctx, ledger);
55
- if (!guardResult.success) {
56
- throw new Error(`Guard failed: ${guardResult.error}`);
61
+ // Check pre-execution guards and collect results
62
+ const guardCheck = await checkGuards(spell.guards, ctx, ledger);
63
+ collectGuardResults(guardResults, spell.guards, guardCheck);
64
+ if (!guardCheck.success) {
65
+ const structuredError = createStructuredError("preview", "GUARD_FAILED", `Guard failed: ${guardCheck.error}`);
66
+ const receipt = buildReceipt({
67
+ id: receiptId,
68
+ spell,
69
+ ctx,
70
+ guardResults,
71
+ advisoryResults,
72
+ plannedActions,
73
+ valueDeltas,
74
+ status: "rejected",
75
+ error: structuredError.message,
76
+ });
77
+ registerIssuedReceipt(receipt);
78
+ ledger.emit({ type: "receipt_generated", receiptId });
79
+ ledger.emit({ type: "preview_completed", runId: ctx.runId, receiptId, status: "rejected" });
80
+ return {
81
+ success: false,
82
+ receipt,
83
+ error: structuredError,
84
+ ledgerEvents: ledger.getEntries(),
85
+ };
57
86
  }
58
- // Build step map for quick lookup
87
+ // Run step loop in preview mode — action steps produce PlannedActions instead of executing
59
88
  const stepMap = new Map(spell.steps.map((s) => [s.id, s]));
60
- // Execute steps in order (topological sort would be ideal, but for now sequential)
61
- for (const step of spell.steps) {
62
- // Skip steps already executed by a parent (try/loop/conditional)
63
- if (ctx.executedSteps.includes(step.id)) {
64
- continue;
65
- }
66
- // Check dependencies
67
- for (const depId of step.dependsOn) {
68
- if (!ctx.executedSteps.includes(depId)) {
69
- throw new Error(`Step '${step.id}' depends on '${depId}' which has not been executed`);
70
- }
71
- }
72
- // Execute step
73
- const result = await executeStep(step, ctx, ledger, stepMap, actionExecution, options.onAdvisory);
74
- // Mark all child steps of container steps (try/loop/conditional) as executed
75
- // so the main loop doesn't re-execute them standalone
76
- for (const childId of getChildStepIds(step)) {
77
- if (!ctx.executedSteps.includes(childId)) {
78
- markStepExecuted(ctx, childId);
79
- }
80
- }
81
- // Enrich step_failed ledger events with source location
82
- if (!result.success) {
83
- const loc = spell.sourceMap?.[step.id];
84
- if (loc) {
85
- enrichStepFailedEvents(ledger, step.id, loc);
86
- }
87
- }
88
- // Handle halt
89
- if (result.halted) {
90
- ledger.emit({
91
- type: "run_completed",
92
- runId: ctx.runId,
93
- success: true,
94
- metrics: ctx.metrics,
95
- });
96
- return {
97
- success: true,
98
- runId: ctx.runId,
99
- startTime: ctx.startTime,
100
- endTime: Date.now(),
101
- duration: Date.now() - ctx.startTime,
102
- metrics: ctx.metrics,
103
- finalState: getPersistentStateObject(ctx),
104
- ledgerEvents: ledger.getEntries(),
105
- };
106
- }
107
- // Handle failure
108
- if (!result.success) {
109
- const onFailure = "onFailure" in step ? step.onFailure : "revert";
110
- const loc = spell.sourceMap?.[step.id];
111
- const locSuffix = loc ? ` at line ${loc.line}, column ${loc.column}` : "";
112
- switch (onFailure) {
113
- case "halt":
114
- throw new Error(`Step '${step.id}' failed${locSuffix}: ${result.error}`);
115
- case "revert":
116
- throw new Error(`Step '${step.id}' failed${locSuffix}: ${result.error}`);
117
- case "skip":
118
- ledger.emit({
119
- type: "step_skipped",
120
- stepId: step.id,
121
- reason: result.error ?? "Unknown error",
122
- });
123
- continue;
124
- case "catch":
125
- // Would be handled by try/catch step
126
- continue;
127
- }
128
- }
129
- markStepExecuted(ctx, step.id);
89
+ const stepLoopResult = await executeStepLoop(spell, ctx, ledger, stepMap, actionExecution, options.onAdvisory, { isPreview: true, plannedActions, valueDeltas, advisoryResults });
90
+ if (!stepLoopResult.success) {
91
+ const structuredError = createStructuredError("preview", "STEP_FAILED", stepLoopResult.error ?? "Step execution failed");
92
+ const receipt = buildReceipt({
93
+ id: receiptId,
94
+ spell,
95
+ ctx,
96
+ guardResults,
97
+ advisoryResults,
98
+ plannedActions,
99
+ valueDeltas,
100
+ status: "rejected",
101
+ error: structuredError.message,
102
+ });
103
+ registerIssuedReceipt(receipt);
104
+ ledger.emit({ type: "receipt_generated", receiptId });
105
+ ledger.emit({ type: "preview_completed", runId: ctx.runId, receiptId, status: "rejected" });
106
+ return {
107
+ success: false,
108
+ receipt,
109
+ error: structuredError,
110
+ ledgerEvents: ledger.getEntries(),
111
+ };
130
112
  }
131
- // Check post-execution guards
132
- const postGuardResult = await checkGuards(spell.guards, ctx, ledger);
133
- if (!postGuardResult.success && postGuardResult.severity === "halt") {
134
- throw new Error(`Post-execution guard failed: ${postGuardResult.error}`);
113
+ // Post-execution guards
114
+ const postGuardCheck = await checkGuards(spell.guards, ctx, ledger);
115
+ if (!postGuardCheck.success && postGuardCheck.severity === "halt") {
116
+ const structuredError = createStructuredError("preview", "POST_GUARD_FAILED", `Post-execution guard failed: ${postGuardCheck.error}`);
117
+ const receipt = buildReceipt({
118
+ id: receiptId,
119
+ spell,
120
+ ctx,
121
+ guardResults,
122
+ advisoryResults,
123
+ plannedActions,
124
+ valueDeltas,
125
+ status: "rejected",
126
+ error: structuredError.message,
127
+ });
128
+ registerIssuedReceipt(receipt);
129
+ ledger.emit({ type: "receipt_generated", receiptId });
130
+ ledger.emit({ type: "preview_completed", runId: ctx.runId, receiptId, status: "rejected" });
131
+ return {
132
+ success: false,
133
+ receipt,
134
+ error: structuredError,
135
+ ledgerEvents: ledger.getEntries(),
136
+ };
137
+ }
138
+ const valueFlow = evaluatePreviewValueFlow(ctx, plannedActions, valueDeltas);
139
+ if (valueFlow.violation) {
140
+ const structuredError = structuredErrorFromValueFlowViolation("preview", valueFlow.violation);
141
+ const receipt = buildReceipt({
142
+ id: receiptId,
143
+ spell,
144
+ ctx,
145
+ guardResults,
146
+ advisoryResults,
147
+ plannedActions,
148
+ valueDeltas,
149
+ status: "rejected",
150
+ error: structuredError.message,
151
+ constraintResults: valueFlow.constraintResults,
152
+ driftKeys: valueFlow.driftKeys,
153
+ requiresApproval: valueFlow.requiresApproval,
154
+ accounting: valueFlow.accounting,
155
+ });
156
+ registerIssuedReceipt(receipt);
157
+ ledger.emit({ type: "receipt_generated", receiptId });
158
+ ledger.emit({ type: "preview_completed", runId: ctx.runId, receiptId, status: "rejected" });
159
+ return {
160
+ success: false,
161
+ receipt,
162
+ error: structuredError,
163
+ ledgerEvents: ledger.getEntries(),
164
+ };
135
165
  }
136
- // Log run completion
166
+ const requiresApproval = valueFlow.requiresApproval;
167
+ const receipt = buildReceipt({
168
+ id: receiptId,
169
+ spell,
170
+ ctx,
171
+ guardResults,
172
+ advisoryResults,
173
+ plannedActions,
174
+ valueDeltas,
175
+ status: "ready",
176
+ constraintResults: valueFlow.constraintResults,
177
+ driftKeys: valueFlow.driftKeys,
178
+ requiresApproval: valueFlow.requiresApproval,
179
+ accounting: valueFlow.accounting,
180
+ });
181
+ registerIssuedReceipt(receipt);
182
+ if (requiresApproval) {
183
+ ledger.emit({
184
+ type: "approval_required",
185
+ receiptId,
186
+ reason: "One or more actions crossed approval_required_above",
187
+ });
188
+ }
189
+ ledger.emit({ type: "receipt_generated", receiptId });
137
190
  ledger.emit({
138
191
  type: "run_completed",
139
192
  runId: ctx.runId,
140
193
  success: true,
141
194
  metrics: ctx.metrics,
142
195
  });
196
+ ledger.emit({ type: "preview_completed", runId: ctx.runId, receiptId, status: "ready" });
197
+ return { success: true, receipt, ledgerEvents: ledger.getEntries() };
198
+ }
199
+ catch (error) {
200
+ const message = error instanceof Error ? error.message : String(error);
201
+ const structuredError = createStructuredError("preview", "PREVIEW_INTERNAL_ERROR", message);
202
+ ledger.emit({ type: "run_failed", runId: ctx.runId, error: message });
203
+ ledger.emit({ type: "preview_completed", runId: ctx.runId, receiptId, status: "rejected" });
204
+ return { success: false, error: structuredError, ledgerEvents: ledger.getEntries() };
205
+ }
206
+ }
207
+ /**
208
+ * Commit a receipt — executes planned actions from the preview.
209
+ */
210
+ export async function commit(options) {
211
+ const { receipt, wallet } = options;
212
+ const runId = receipt.id.replace("rcpt_", "");
213
+ const ledger = new InMemoryLedger(runId, receipt.spellId, options.eventCallback);
214
+ ledger.emit({ type: "commit_started", runId, receiptId: receipt.id });
215
+ // Validate receipt status
216
+ if (receipt.status !== "ready") {
217
+ const structuredError = createStructuredError("commit", "RECEIPT_INVALID_STATUS", `Receipt status is '${receipt.status}', expected 'ready'`);
218
+ ledger.emit({ type: "commit_completed", runId, receiptId: receipt.id, success: false });
143
219
  return {
144
- success: true,
145
- runId: ctx.runId,
146
- startTime: ctx.startTime,
147
- endTime: Date.now(),
148
- duration: Date.now() - ctx.startTime,
149
- metrics: ctx.metrics,
150
- finalState: getPersistentStateObject(ctx),
220
+ success: false,
221
+ receiptId: receipt.id,
222
+ transactions: [],
223
+ driftChecks: [],
224
+ finalState: receipt.finalState,
151
225
  ledgerEvents: ledger.getEntries(),
226
+ error: structuredError,
152
227
  };
153
228
  }
154
- catch (error) {
155
- const message = error instanceof Error ? error.message : String(error);
156
- ledger.emit({
157
- type: "run_failed",
158
- runId: ctx.runId,
159
- error: message,
160
- });
229
+ const receiptValidationError = validateCommitReceipt(receipt);
230
+ if (receiptValidationError) {
231
+ ledger.emit({ type: "commit_completed", runId, receiptId: receipt.id, success: false });
161
232
  return {
162
233
  success: false,
163
- runId: ctx.runId,
164
- startTime: ctx.startTime,
165
- endTime: Date.now(),
166
- duration: Date.now() - ctx.startTime,
167
- error: message,
168
- metrics: ctx.metrics,
169
- finalState: getPersistentStateObject(ctx),
234
+ receiptId: receipt.id,
235
+ transactions: [],
236
+ driftChecks: [],
237
+ finalState: receipt.finalState,
170
238
  ledgerEvents: ledger.getEntries(),
239
+ error: receiptValidationError,
171
240
  };
172
241
  }
242
+ // Check receipt age
243
+ if (options.driftPolicy?.maxAge) {
244
+ const ageSec = (Date.now() - receipt.timestamp) / 1000;
245
+ if (ageSec > options.driftPolicy.maxAge) {
246
+ const structuredError = createStructuredError("commit", "RECEIPT_EXPIRED", `Receipt expired: age ${Math.round(ageSec)}s exceeds maxAge ${options.driftPolicy.maxAge}s`, {
247
+ actual: Math.round(ageSec),
248
+ limit: options.driftPolicy.maxAge,
249
+ suggestion: "Run preview again to generate a fresh receipt.",
250
+ });
251
+ ledger.emit({ type: "commit_completed", runId, receiptId: receipt.id, success: false });
252
+ return {
253
+ success: false,
254
+ receiptId: receipt.id,
255
+ transactions: [],
256
+ driftChecks: [],
257
+ finalState: receipt.finalState,
258
+ ledgerEvents: ledger.getEntries(),
259
+ error: structuredError,
260
+ };
261
+ }
262
+ }
263
+ const driftChecks = [];
264
+ for (const driftKey of receipt.driftKeys) {
265
+ if (options.driftPolicy?.maxAge) {
266
+ const keyAgeSec = Math.max(0, Math.floor((Date.now() - driftKey.timestamp) / 1000));
267
+ if (keyAgeSec > options.driftPolicy.maxAge) {
268
+ const structuredError = createStructuredError("commit", "DRIFT_KEY_STALE", `Drift key '${driftKey.field}' is stale (${keyAgeSec}s > ${options.driftPolicy.maxAge}s)`, {
269
+ constraint: "drift_key_freshness",
270
+ actual: keyAgeSec,
271
+ limit: options.driftPolicy.maxAge,
272
+ path: driftKey.field,
273
+ suggestion: "Run preview again to refresh drift keys.",
274
+ });
275
+ ledger.emit({ type: "commit_completed", runId, receiptId: receipt.id, success: false });
276
+ return {
277
+ success: false,
278
+ receiptId: receipt.id,
279
+ transactions: [],
280
+ driftChecks,
281
+ finalState: receipt.finalState,
282
+ ledgerEvents: ledger.getEntries(),
283
+ error: structuredError,
284
+ };
285
+ }
286
+ }
287
+ let resolvedValue;
288
+ try {
289
+ resolvedValue = await resolveCommitDriftValue(driftKey, options);
290
+ }
291
+ catch (error) {
292
+ const message = error instanceof Error ? error.message : String(error);
293
+ const structuredError = createStructuredError("commit", "DRIFT_RESOLUTION_FAILED", `Failed to resolve drift value for '${driftKey.field}': ${message}`, {
294
+ constraint: "drift_keys",
295
+ path: driftKey.field,
296
+ });
297
+ ledger.emit({ type: "commit_completed", runId, receiptId: receipt.id, success: false });
298
+ return {
299
+ success: false,
300
+ receiptId: receipt.id,
301
+ transactions: [],
302
+ driftChecks,
303
+ finalState: receipt.finalState,
304
+ ledgerEvents: ledger.getEntries(),
305
+ error: structuredError,
306
+ };
307
+ }
308
+ if (!resolvedValue.found && options.driftPolicy) {
309
+ const structuredError = createStructuredError("commit", "DRIFT_VALUE_MISSING", `Missing commit-time drift value for '${driftKey.field}'`, {
310
+ constraint: "drift_keys",
311
+ path: driftKey.field,
312
+ suggestion: "Provide driftValues for this key or configure resolveDriftValue to fetch commit-time values.",
313
+ });
314
+ ledger.emit({ type: "commit_completed", runId, receiptId: receipt.id, success: false });
315
+ return {
316
+ success: false,
317
+ receiptId: receipt.id,
318
+ transactions: [],
319
+ driftChecks,
320
+ finalState: receipt.finalState,
321
+ ledgerEvents: ledger.getEntries(),
322
+ error: structuredError,
323
+ };
324
+ }
325
+ const commitValue = resolvedValue.found ? resolvedValue.value : driftKey.previewValue;
326
+ const driftResult = evaluateDriftKey(driftKey, commitValue, options.driftPolicy);
327
+ driftChecks.push(driftResult);
328
+ ledger.emit({
329
+ type: "drift_check",
330
+ field: driftKey.field,
331
+ passed: driftResult.passed,
332
+ previewValue: driftResult.previewValue,
333
+ commitValue: driftResult.commitValue,
334
+ });
335
+ if (!driftResult.passed) {
336
+ const tolerance = resolveToleranceBps(driftKey, options.driftPolicy);
337
+ const structuredError = createStructuredError("commit", "DRIFT_EXCEEDED", `Drift exceeded for '${driftKey.field}'`, {
338
+ constraint: "drift_policy",
339
+ actual: driftResult.driftBps,
340
+ limit: tolerance,
341
+ path: driftKey.field,
342
+ suggestion: "Run preview again or increase drift tolerance for this key class.",
343
+ });
344
+ ledger.emit({ type: "commit_completed", runId, receiptId: receipt.id, success: false });
345
+ return {
346
+ success: false,
347
+ receiptId: receipt.id,
348
+ transactions: [],
349
+ driftChecks,
350
+ finalState: receipt.finalState,
351
+ ledgerEvents: ledger.getEntries(),
352
+ error: structuredError,
353
+ };
354
+ }
355
+ }
356
+ // Execute planned actions
357
+ const { chainId } = receipt.chainContext;
358
+ const provider = options.provider ?? createProvider(chainId, options.rpcUrl);
359
+ const executor = createExecutor({
360
+ wallet,
361
+ provider,
362
+ mode: "execute",
363
+ gasMultiplier: options.gasMultiplier,
364
+ confirmCallback: options.confirmCallback,
365
+ progressCallback: options.progressCallback,
366
+ skipTestnetConfirmation: options.skipTestnetConfirmation,
367
+ adapters: options.adapters,
368
+ });
369
+ const transactions = [];
370
+ for (const planned of receipt.plannedActions) {
371
+ try {
372
+ const txResult = await commitActionStep(planned, executor);
373
+ if (txResult.success) {
374
+ transactions.push({
375
+ stepId: planned.stepId,
376
+ hash: txResult.hash,
377
+ gasUsed: txResult.gasUsed,
378
+ success: true,
379
+ });
380
+ if (txResult.hash) {
381
+ ledger.emit({
382
+ type: "action_submitted",
383
+ action: planned.action,
384
+ txHash: txResult.hash,
385
+ });
386
+ }
387
+ if (txResult.gasUsed !== undefined) {
388
+ ledger.emit({
389
+ type: "action_confirmed",
390
+ txHash: txResult.hash ?? "",
391
+ gasUsed: txResult.gasUsed.toString(),
392
+ });
393
+ }
394
+ }
395
+ else {
396
+ transactions.push({
397
+ stepId: planned.stepId,
398
+ hash: txResult.hash,
399
+ success: false,
400
+ error: txResult.error,
401
+ });
402
+ if (planned.onFailure === "skip") {
403
+ ledger.emit({
404
+ type: "step_skipped",
405
+ stepId: planned.stepId,
406
+ reason: txResult.error ?? "Action execution failed",
407
+ });
408
+ continue;
409
+ }
410
+ const structuredError = createStructuredError("commit", "ACTION_COMMIT_FAILED", `Action step '${planned.stepId}' failed: ${txResult.error}`);
411
+ ledger.emit({ type: "commit_completed", runId, receiptId: receipt.id, success: false });
412
+ return {
413
+ success: false,
414
+ receiptId: receipt.id,
415
+ transactions,
416
+ driftChecks,
417
+ finalState: receipt.finalState,
418
+ ledgerEvents: ledger.getEntries(),
419
+ error: structuredError,
420
+ };
421
+ }
422
+ }
423
+ catch (error) {
424
+ const message = error instanceof Error ? error.message : String(error);
425
+ transactions.push({ stepId: planned.stepId, success: false, error: message });
426
+ const structuredError = createStructuredError("commit", "COMMIT_INTERNAL_ERROR", message);
427
+ ledger.emit({ type: "commit_completed", runId, receiptId: receipt.id, success: false });
428
+ return {
429
+ success: false,
430
+ receiptId: receipt.id,
431
+ transactions,
432
+ driftChecks,
433
+ finalState: receipt.finalState,
434
+ ledgerEvents: ledger.getEntries(),
435
+ error: structuredError,
436
+ };
437
+ }
438
+ }
439
+ committedReceipts.add(receipt.id);
440
+ ledger.emit({ type: "commit_completed", runId, receiptId: receipt.id, success: true });
441
+ return {
442
+ success: true,
443
+ receiptId: receipt.id,
444
+ transactions,
445
+ driftChecks,
446
+ finalState: receipt.finalState,
447
+ ledgerEvents: ledger.getEntries(),
448
+ };
449
+ }
450
+ // =============================================================================
451
+ // EXECUTE (backward-compatible wrapper)
452
+ // =============================================================================
453
+ /**
454
+ * Execute a compiled spell (backward-compatible wrapper).
455
+ * Internally uses preview() and, if needed, commit().
456
+ */
457
+ export async function execute(options) {
458
+ const { spell, vault, chain, params = {}, persistentState = {}, simulate = false } = options;
459
+ const actionMode = resolveExecutionMode(options, simulate);
460
+ const previewResult = await preview({
461
+ spell,
462
+ vault,
463
+ chain,
464
+ params,
465
+ persistentState,
466
+ trigger: options.trigger,
467
+ adapters: options.adapters,
468
+ policy: options.policy,
469
+ advisorSkillsDirs: options.advisorSkillsDirs,
470
+ onAdvisory: options.onAdvisory,
471
+ progressCallback: options.progressCallback,
472
+ eventCallback: options.eventCallback,
473
+ });
474
+ if (!previewResult.success || !previewResult.receipt) {
475
+ return convertPreviewToExecutionResult(previewResult, spell);
476
+ }
477
+ const receipt = previewResult.receipt;
478
+ // Preview-only execution for simulate/dry-run/no-wallet and compute-only spells.
479
+ if (actionMode === "simulate" ||
480
+ actionMode === "dry-run" ||
481
+ !options.wallet ||
482
+ receipt.plannedActions.length === 0) {
483
+ return convertPreviewToExecutionResult(previewResult, spell);
484
+ }
485
+ const commitResult = await commit({
486
+ receipt,
487
+ wallet: options.wallet,
488
+ provider: options.provider,
489
+ rpcUrl: options.rpcUrl,
490
+ gasMultiplier: options.gasMultiplier,
491
+ adapters: options.adapters,
492
+ confirmCallback: options.confirmCallback,
493
+ progressCallback: options.progressCallback,
494
+ skipTestnetConfirmation: options.skipTestnetConfirmation,
495
+ eventCallback: options.eventCallback,
496
+ });
497
+ return convertPreviewCommitToExecutionResult(previewResult, commitResult);
498
+ }
499
+ /**
500
+ * Shared step execution loop used by both preview() and execute().
501
+ */
502
+ async function executeStepLoop(spell, ctx, ledger, stepMap, actionExecution, advisoryHandler, collectors) {
503
+ for (const step of spell.steps) {
504
+ if (ctx.executedSteps.includes(step.id)) {
505
+ continue;
506
+ }
507
+ for (const depId of step.dependsOn) {
508
+ if (!ctx.executedSteps.includes(depId)) {
509
+ return {
510
+ success: false,
511
+ error: `Step '${step.id}' depends on '${depId}' which has not been executed`,
512
+ };
513
+ }
514
+ }
515
+ // In preview mode, action steps go through previewActionStep
516
+ let result;
517
+ if (collectors?.isPreview && step.kind === "action") {
518
+ const previewResult = await previewActionStep(step, ctx, ledger, actionExecution);
519
+ result = previewResult.stepResult;
520
+ if (previewResult.plannedAction) {
521
+ collectors.plannedActions?.push(previewResult.plannedAction);
522
+ }
523
+ if (previewResult.valueDeltas?.length) {
524
+ collectors.valueDeltas?.push(...previewResult.valueDeltas);
525
+ for (const delta of previewResult.valueDeltas) {
526
+ ledger.emit({ type: "value_delta", delta });
527
+ }
528
+ }
529
+ }
530
+ else if (collectors?.isPreview && step.kind === "advisory") {
531
+ result = await executeAdvisoryStep(step, ctx, ledger, advisoryHandler);
532
+ if (result.success) {
533
+ collectors.advisoryResults?.push({
534
+ stepId: step.id,
535
+ advisor: step.advisor,
536
+ output: result.output,
537
+ fallback: result.fallback ?? false,
538
+ rawOutput: result.rawOutput,
539
+ effectiveOutput: result.effectiveOutput ?? result.output,
540
+ onViolation: result.violationPolicy ?? step.violationPolicy ?? "reject",
541
+ policyScope: step.policyScope,
542
+ clampConstraints: step.clampConstraints,
543
+ clamped: result.clamped ?? false,
544
+ violations: result.advisoryViolations,
545
+ });
546
+ }
547
+ }
548
+ else {
549
+ result = await executeStep(step, ctx, ledger, stepMap, actionExecution, advisoryHandler);
550
+ }
551
+ for (const childId of getChildStepIds(step)) {
552
+ if (!ctx.executedSteps.includes(childId)) {
553
+ markStepExecuted(ctx, childId);
554
+ }
555
+ }
556
+ if (!result.success) {
557
+ const loc = spell.sourceMap?.[step.id];
558
+ if (loc) {
559
+ enrichStepFailedEvents(ledger, step.id, loc);
560
+ }
561
+ }
562
+ if (result.halted) {
563
+ return { success: true, halted: true };
564
+ }
565
+ if (!result.success) {
566
+ const onFailure = "onFailure" in step ? step.onFailure : "revert";
567
+ const loc = spell.sourceMap?.[step.id];
568
+ const locSuffix = loc ? ` at line ${loc.line}, column ${loc.column}` : "";
569
+ switch (onFailure) {
570
+ case "halt":
571
+ return { success: false, error: `Step '${step.id}' failed${locSuffix}: ${result.error}` };
572
+ case "revert":
573
+ return { success: false, error: `Step '${step.id}' failed${locSuffix}: ${result.error}` };
574
+ case "skip":
575
+ ledger.emit({
576
+ type: "step_skipped",
577
+ stepId: step.id,
578
+ reason: result.error ?? "Unknown error",
579
+ });
580
+ continue;
581
+ case "catch":
582
+ continue;
583
+ }
584
+ }
585
+ markStepExecuted(ctx, step.id);
586
+ }
587
+ return { success: true };
588
+ }
589
+ // =============================================================================
590
+ // HELPERS
591
+ // =============================================================================
592
+ function convertPreviewToExecutionResult(previewResult, _spell) {
593
+ const receipt = previewResult.receipt;
594
+ const now = Date.now();
595
+ const startTime = receipt?.timestamp ?? now;
596
+ return {
597
+ success: previewResult.success,
598
+ runId: receipt?.id.replace("rcpt_", "") ?? `preview_${now}`,
599
+ startTime,
600
+ endTime: now,
601
+ duration: now - startTime,
602
+ error: previewResult.error ? formatStructuredError(previewResult.error) : undefined,
603
+ structuredError: previewResult.error,
604
+ metrics: receipt?.metrics ?? {
605
+ stepsExecuted: 0,
606
+ actionsExecuted: 0,
607
+ gasUsed: 0n,
608
+ advisoryCalls: 0,
609
+ errors: 0,
610
+ retries: 0,
611
+ },
612
+ finalState: receipt?.finalState ?? {},
613
+ ledgerEvents: previewResult.ledgerEvents,
614
+ receipt,
615
+ };
616
+ }
617
+ function convertPreviewCommitToExecutionResult(previewResult, commitResult) {
618
+ const receipt = previewResult.receipt;
619
+ const endTime = Date.now();
620
+ const startTime = receipt?.timestamp ?? endTime;
621
+ const txGasUsed = commitResult.transactions.reduce((sum, tx) => sum + (tx.gasUsed ?? 0n), 0n);
622
+ const baseMetrics = receipt?.metrics ?? {
623
+ stepsExecuted: 0,
624
+ actionsExecuted: 0,
625
+ gasUsed: 0n,
626
+ advisoryCalls: 0,
627
+ errors: 0,
628
+ retries: 0,
629
+ };
630
+ return {
631
+ success: commitResult.success,
632
+ runId: receipt?.id.replace("rcpt_", "") ?? `commit_${endTime}`,
633
+ startTime,
634
+ endTime,
635
+ duration: endTime - startTime,
636
+ error: commitResult.error ? formatStructuredError(commitResult.error) : undefined,
637
+ structuredError: commitResult.error,
638
+ metrics: {
639
+ ...baseMetrics,
640
+ gasUsed: baseMetrics.gasUsed + txGasUsed,
641
+ },
642
+ finalState: commitResult.finalState,
643
+ ledgerEvents: [...previewResult.ledgerEvents, ...commitResult.ledgerEvents],
644
+ receipt,
645
+ commit: commitResult,
646
+ };
647
+ }
648
+ function buildReceipt(opts) {
649
+ return {
650
+ id: opts.id,
651
+ spellId: opts.spell.id,
652
+ phase: "preview",
653
+ timestamp: Date.now(),
654
+ chainContext: {
655
+ chainId: opts.ctx.chain,
656
+ vault: opts.ctx.vault,
657
+ },
658
+ guardResults: opts.guardResults,
659
+ advisoryResults: opts.advisoryResults,
660
+ plannedActions: opts.plannedActions,
661
+ valueDeltas: opts.valueDeltas,
662
+ accounting: opts.accounting ??
663
+ {
664
+ assets: [],
665
+ totalUnaccounted: 0n,
666
+ passed: true,
667
+ },
668
+ constraintResults: opts.constraintResults ?? [],
669
+ driftKeys: opts.driftKeys ?? [],
670
+ requiresApproval: opts.requiresApproval ?? false,
671
+ status: opts.status,
672
+ metrics: { ...opts.ctx.metrics },
673
+ finalState: getPersistentStateObject(opts.ctx),
674
+ error: opts.error,
675
+ };
676
+ }
677
+ function collectGuardResults(guardResults, guards, check) {
678
+ for (const guard of guards) {
679
+ guardResults.push({
680
+ guardId: guard.id,
681
+ passed: check.success,
682
+ severity: check.severity ?? ("severity" in guard ? String(guard.severity) : "warn"),
683
+ message: check.success ? undefined : check.error,
684
+ });
685
+ }
173
686
  }
174
687
  function resolveExecutionMode(options, simulate) {
175
688
  if (options.executionMode) {
@@ -183,25 +696,159 @@ function resolveExecutionMode(options, simulate) {
183
696
  }
184
697
  return "simulate";
185
698
  }
186
- function createActionExecutionOptions(options, mode, chainId) {
187
- if (mode === "simulate") {
188
- return { mode };
699
+ function createStructuredError(phase, code, message, extras) {
700
+ return {
701
+ phase,
702
+ code,
703
+ message,
704
+ ...extras,
705
+ };
706
+ }
707
+ function structuredErrorFromValueFlowViolation(phase, violation) {
708
+ return createStructuredError(phase, violation.code, violation.message, {
709
+ constraint: violation.constraint,
710
+ actual: violation.actual,
711
+ limit: violation.limit,
712
+ path: violation.path,
713
+ suggestion: violation.suggestion,
714
+ });
715
+ }
716
+ function formatStructuredError(error) {
717
+ return `[${error.code}] ${error.message}`;
718
+ }
719
+ function registerIssuedReceipt(receipt) {
720
+ issuedReceipts.set(receipt.id, {
721
+ spellId: receipt.spellId,
722
+ chainId: receipt.chainContext.chainId,
723
+ vault: receipt.chainContext.vault,
724
+ timestamp: receipt.timestamp,
725
+ });
726
+ }
727
+ function validateCommitReceipt(receipt) {
728
+ if (receipt.phase !== "preview") {
729
+ return createStructuredError("commit", "RECEIPT_INVALID_PHASE", `Receipt phase is '${receipt.phase}', expected 'preview'`);
189
730
  }
190
- if (!options.wallet) {
191
- throw new Error("Wallet is required for non-simulated execution");
731
+ if (!receipt.id.startsWith("rcpt_")) {
732
+ return createStructuredError("commit", "RECEIPT_INVALID_ID", "Receipt ID must start with 'rcpt_'");
192
733
  }
193
- const provider = options.provider ?? createProvider(chainId, options.rpcUrl);
194
- const executor = createExecutor({
195
- wallet: options.wallet,
196
- provider,
197
- mode,
198
- gasMultiplier: options.gasMultiplier,
199
- confirmCallback: options.confirmCallback,
200
- progressCallback: options.progressCallback,
201
- skipTestnetConfirmation: options.skipTestnetConfirmation,
202
- adapters: options.adapters,
203
- });
204
- return { mode, executor };
734
+ if (committedReceipts.has(receipt.id)) {
735
+ return createStructuredError("commit", "RECEIPT_ALREADY_COMMITTED", "Receipt has already been committed.");
736
+ }
737
+ const issuedReceipt = issuedReceipts.get(receipt.id);
738
+ if (!issuedReceipt) {
739
+ return createStructuredError("commit", "PREVIEW_RECEIPT_UNKNOWN", "Commit requires a valid preview receipt generated by this runtime.");
740
+ }
741
+ if (issuedReceipt.spellId !== receipt.spellId ||
742
+ issuedReceipt.chainId !== receipt.chainContext.chainId ||
743
+ issuedReceipt.vault !== receipt.chainContext.vault ||
744
+ issuedReceipt.timestamp !== receipt.timestamp) {
745
+ return createStructuredError("commit", "PREVIEW_RECEIPT_TAMPERED", "Receipt identity does not match the preview-generated artifact.");
746
+ }
747
+ return undefined;
748
+ }
749
+ async function resolveCommitDriftValue(driftKey, options) {
750
+ if (options.driftValues &&
751
+ Object.prototype.hasOwnProperty.call(options.driftValues, driftKey.field)) {
752
+ return { found: true, value: options.driftValues[driftKey.field] };
753
+ }
754
+ if (options.resolveDriftValue) {
755
+ const value = await options.resolveDriftValue(driftKey);
756
+ if (value !== undefined) {
757
+ return { found: true, value };
758
+ }
759
+ }
760
+ return { found: false, value: undefined };
761
+ }
762
+ function evaluateDriftKey(driftKey, commitValue, policy) {
763
+ const tolerance = resolveToleranceBps(driftKey, policy);
764
+ const numericPreview = toNumeric(driftKey.previewValue);
765
+ const numericCommit = toNumeric(commitValue);
766
+ if (tolerance !== undefined && numericPreview !== undefined && numericCommit !== undefined) {
767
+ const driftBps = computeDriftBps(numericPreview, numericCommit);
768
+ return {
769
+ field: driftKey.field,
770
+ passed: driftBps <= tolerance,
771
+ previewValue: driftKey.previewValue,
772
+ commitValue,
773
+ driftBps,
774
+ };
775
+ }
776
+ if (tolerance !== undefined) {
777
+ return {
778
+ field: driftKey.field,
779
+ passed: false,
780
+ previewValue: driftKey.previewValue,
781
+ commitValue,
782
+ };
783
+ }
784
+ return {
785
+ field: driftKey.field,
786
+ passed: valuesEquivalent(driftKey.previewValue, commitValue),
787
+ previewValue: driftKey.previewValue,
788
+ commitValue,
789
+ };
790
+ }
791
+ function resolveToleranceBps(driftKey, policy) {
792
+ if (!policy)
793
+ return undefined;
794
+ const driftClass = driftKey.class ?? inferDriftClass(driftKey.field);
795
+ switch (driftClass) {
796
+ case "balance":
797
+ return policy.balance?.toleranceBps;
798
+ case "quote":
799
+ return policy.quote?.toleranceBps;
800
+ case "rate":
801
+ return policy.rate?.toleranceBps;
802
+ case "gas":
803
+ return policy.gas?.toleranceBps;
804
+ default:
805
+ return undefined;
806
+ }
807
+ }
808
+ function toNumeric(value) {
809
+ if (typeof value === "bigint") {
810
+ return value;
811
+ }
812
+ if (typeof value === "number") {
813
+ if (!Number.isFinite(value))
814
+ return undefined;
815
+ return BigInt(Math.trunc(value));
816
+ }
817
+ if (typeof value === "string") {
818
+ const trimmed = value.trim();
819
+ if (!/^[-+]?\d+$/.test(trimmed))
820
+ return undefined;
821
+ try {
822
+ return BigInt(trimmed);
823
+ }
824
+ catch {
825
+ return undefined;
826
+ }
827
+ }
828
+ return undefined;
829
+ }
830
+ function computeDriftBps(preview, commit) {
831
+ const delta = preview >= commit ? preview - commit : commit - preview;
832
+ if (preview === 0n) {
833
+ return delta === 0n ? 0 : 10_000;
834
+ }
835
+ return Number((delta * 10000n) / (preview >= 0n ? preview : -preview));
836
+ }
837
+ function valuesEquivalent(left, right) {
838
+ if (typeof left === "bigint" || typeof right === "bigint") {
839
+ const leftBigint = toNumeric(left);
840
+ const rightBigint = toNumeric(right);
841
+ if (leftBigint === undefined || rightBigint === undefined)
842
+ return false;
843
+ return leftBigint === rightBigint;
844
+ }
845
+ if (typeof left === "number" && typeof right === "number") {
846
+ return Number.isFinite(left) && Number.isFinite(right) && left === right;
847
+ }
848
+ if (typeof left === "string" && typeof right === "string") {
849
+ return left === right;
850
+ }
851
+ return Object.is(left, right);
205
852
  }
206
853
  function buildAdvisorTooling(advisors, searchDirs) {
207
854
  if (!advisors.length)
@@ -397,8 +1044,6 @@ async function checkGuards(guards, ctx, ledger) {
397
1044
  }
398
1045
  /**
399
1046
  * Enrich step_failed ledger events with source location info.
400
- * Scans recent events to find step_failed events for the given stepId
401
- * and adds line/column from the source map.
402
1047
  */
403
1048
  function enrichStepFailedEvents(ledger, stepId, loc) {
404
1049
  const entries = ledger.getEntries();