Files
Fran Algaba 8ab7adaa19 Add stable trigger selectors for per-handler execution (#92)
* Add stable trigger selectors to runtime and CLI

* Update Grimoire skill docs for trigger selectors

* Add trigger discovery CLI command

* Fix --no-state CLI flag parsing
2026-04-09 15:23:49 +02:00

9.2 KiB

Compiler and Runtime API Reference

This page documents the programmatic API exported by @grimoirelabs/core.

Compiler API

Source: packages/core/src/compiler/index.ts

compile(source: string): CompilationResult

Compiles Grimoire source text.

Stages:

  1. parse AST
  2. transform to SpellSource
  3. generate IR
  4. type-check IR
  5. validate IR

Returns:

  • success
  • ir when successful
  • errors
  • warnings

compileFile(filePath: string): Promise<CompilationResult>

Reads a .spell file and compiles it.

parseSpell(content: string): ParseResult

Parses source into SpellSource (without full compile/validate pipeline).

parseSpellFile(filePath: string): Promise<ParseResult>

File path variant for parse-only workflow.

Expression helpers

  • parseExpression
  • tryParseExpression

Runtime API

Source: packages/core/src/runtime/interpreter.ts

Runtime semantics are consistent across CLI and library entry points.

preview(options: PreviewOptions): Promise<PreviewResult>

Runs full spell in simulation mode and emits a receipt.

Key responsibilities:

  • guard checks
  • step execution loop (actions as planned actions)
  • advisory output capture
  • value-flow evaluation and constraints
  • receipt generation

Accepts an optional queryProvider for blockchain data queries (see Query Provider API below).

Selected-trigger execution:

  • PreviewOptions.selectedTrigger accepts exactly one of { id, index, label }
  • id is the canonical stable selector exported by compilation
  • index is the 0-based compile order for the handler
  • label is a legacy alias and may be ambiguous for duplicate handlers
  • PreviewResult.selectedTrigger echoes the resolved handler selector

commit(options: CommitOptions): Promise<CommitResult>

Commits planned actions from a ready preview receipt.

Validation and safety gates:

  • receipt status/identity checks
  • in-process preview receipt provenance checks
  • drift checks and optional max-age policy

buildTransactions(options: BuildTransactionsOptions): Promise<BuildTransactionsResult>

Builds unsigned transaction calldata from a ready preview receipt, without committing it.

Designed for client-side signing flows where a server previews and the client signs:

preview() → persist receipt → buildTransactions() → sign (client) → broadcast

Validation and safety gates:

  • receipt status, phase, and ID format checks
  • same-process tamper detection via issued-receipt registry
  • cross-process integrity verification via HMAC (see signReceipt() below)
  • already-committed receipt rejection
  • provider chain must match receipt chain
  • drift checks (same logic as commit())

Behavior differences from commit():

  • does not require a Wallet — only needs walletAddress for calldata construction
  • does not mark the receipt as committed (calling commit() afterwards still works)
  • rejects offchain-only adapters (they produce no signable calldata; use commit() instead)
  • defers provider creation until an EVM action needs gas estimation
  • forwards receipt.chainContext.vault to adapter context for correct recipient routing

Key options:

  • receipt: the preview receipt to build from
  • walletAddress: signing address for calldata construction
  • provider?: EVM provider (must match receipt chain if provided)
  • adapters?: venue adapters for action building
  • receiptSecret? + receiptIntegrity?: required for cross-process receipts (see below)
  • driftPolicy?, driftValues?, resolveDriftValue?: drift check configuration

signReceipt(receipt: Receipt, secret: string): string

Computes an HMAC-SHA256 integrity hash over a receipt's tamper-critical fields.

Call at preview time, persist the returned hex string alongside the receipt, and pass it as receiptIntegrity to buildTransactions() when the receipt crosses process boundaries.

Fields covered by the hash: id, spellId, chainContext, timestamp, status, plannedActions (including each action's type, venue, amounts, and constraints).

Cross-process flow:

// Server A: preview and sign
const result = await preview({ spell, vault, chain, adapters });
const integrity = signReceipt(result.receipt, process.env.RECEIPT_SECRET);
// persist receipt + integrity, respond to client

// Server B (or later request): build transactions
const buildResult = await buildTransactions({
  receipt: persistedReceipt,
  walletAddress: signerAddress,
  adapters,
  receiptSecret: process.env.RECEIPT_SECRET,
  receiptIntegrity: persistedIntegrity,
});

execute(options: ExecuteOptions): Promise<ExecutionResult>

Backward-compatible wrapper that also accepts an optional queryProvider:

  • always runs preview()
  • commits only if runtime mode requires commit and wallet is present

Execution-mode behavior:

  • simulate -> preview only
  • dry-run -> preview only
  • execute + wallet + planned actions -> preview + commit

ExecuteOptions.selectedTrigger uses the same selector contract as preview().

Session API

Source: packages/core/src/runtime/session.ts

  • runSession
  • runOneShotSession
  • runManagedSession

These normalize trigger metadata for one-shot and managed runs while reusing execute() semantics.

State Store API

Source: packages/core/src/runtime/state-store.ts

StateStore contract:

  • load(spellId)
  • save(spellId, state)
  • addRun(spellId, run)
  • getRuns(spellId, limit?)
  • saveLedger(spellId, runId, entries)
  • loadLedger(spellId, runId)
  • listSpells()

Helper:

  • createRunRecord(result, provenance?)

SQLite implementation:

  • SqliteStateStore in runtime/sqlite-state-store.ts
  • Bun path: bun:sqlite
  • Node fallback: better-sqlite3

Key Runtime Types

SpellIR

Defines compiled spell metadata, config, steps, guards, triggers, source map, and trigger handler selectors.

Relevant trigger fields:

  • triggers: legacy trigger graph representation
  • triggerStepMap: legacy mapping from multi-trigger handler index to top-level step ids
  • triggerHandlers: stable per-handler metadata:
    • selector.id
    • selector.index
    • selector.label
    • selector.source
    • trigger
    • stepIds

ExecutionResult

Includes:

  • run IDs and duration
  • success/error info
  • metrics
  • final state
  • ledger events
  • selected trigger metadata (selectedTrigger) when a handler was selected
  • preview receipt
  • commit result (when applicable)

Receipt

Preview artifact includes:

  • guard and advisory results
  • planned actions
  • value deltas and accounting
  • constraint results
  • drift keys
  • approval requirement signal
  • final state and metrics

Query Provider API

Source: packages/core/src/types/query-provider.ts

QueryProvider is a pluggable interface that supplies blockchain data (balances, prices, APY, etc.) to spell expressions at runtime.

QueryProvider

interface QueryProvider {
  meta: QueryProviderMeta;
  queryBalance?: (asset: string, address?: string) => Promise<bigint>;
  queryPrice?: (base: string, quote: string, source?: string) => Promise<number>;
  queryMetric?: (request: MetricRequest) => Promise<number>;
  queryHealthFactor?: (venue: string) => Promise<number>;
  queryPosition?: (venue: string, asset: string) => Promise<unknown>;
  queryDebt?: (venue: string, asset: string) => Promise<bigint>;
}

All query methods are optional. The provider declares which queries it supports via meta.supportedQueries.

QueryProviderMeta

interface QueryProviderMeta {
  name: string;
  supportedQueries: Array<"balance" | "price" | "metric" | "health_factor" | "position" | "debt">;
  supportedMetrics?: string[];
  description?: string;
}

Data flow

  1. Caller passes queryProvider on PreviewOptions or ExecuteOptions.
  2. createContext() copies it onto ExecutionContext.queryProvider.
  3. createEvalContext() binds each provider method onto the EvalContext.
  4. Expression evaluator maps spell functions (balance(), price(), apy(), metric(), health_factor(), position(), debt()) to the corresponding query* method on EvalContext.

If a spell calls a query function and no provider (or no matching method) is available, the evaluator throws at runtime.

Wallet and Provider API

Source: packages/core/src/wallet/*

Exports include:

  • key loading and keystore helpers
  • wallet creation
  • provider creation
  • transaction builder
  • executor

Execution modes:

  • simulate
  • dry-run
  • execute

Executor delegates venue-specific action building/execution to adapter registry when available.

Builder API

@grimoirelabs/core exports fluent builders from packages/core/src/builders for programmatic spell construction.

Examples of exported builder helpers:

  • spell
  • action
  • compute
  • conditional
  • repeat
  • forLoop
  • parallel
  • pipeline
  • advisory
  • emit

Minimal Example

import { compile, execute } from "@grimoirelabs/core";
import { adapters } from "@grimoirelabs/venues";

const compiled = compile(sourceText);
if (!compiled.success || !compiled.ir) throw new Error("compile failed");

const result = await execute({
  spell: compiled.ir,
  vault: "0x0000000000000000000000000000000000000000",
  chain: 1,
  params: {},
  simulate: true,
  adapters,
});

console.log(result.success, result.receipt?.status);