Skip to content

The Tenor Go SDK evaluates contracts locally via a wazero WASM bridge --- pure Go, no CGo, no system dependencies. The evaluator runs the same compiled .tenor.wasm artifact as every other SDK, with identical results guaranteed by the cross-SDK conformance suite.

Installation

bash
go get github.com/riverline-labs/tenor/sdks/go

Requires Go 1.21 or later. No CGo. No system libraries. The wazero runtime is a pure Go WebAssembly interpreter/compiler, so the SDK works everywhere Go compiles: Linux, macOS, Windows, ARM, containers, scratch Docker images.

NewEvaluator — Local WASM Evaluation

NewEvaluator loads a compiled contract and evaluates it in-process. No network calls, no API key.

Loading a Contract

go
package main

import (
	"context"
	"fmt"
	"log"
	"os"

	tenor "github.com/riverline-labs/tenor/sdks/go"
)

func main() {
	ctx := context.Background()

	// Load from file path
	evaluator, err := tenor.NewEvaluator(ctx, "./contracts/escrow.tenor.wasm")
	if err != nil {
		log.Fatalf("failed to load contract: %v", err)
	}
	defer evaluator.Close(ctx)

	// Load from bytes
	wasmBytes, err := os.ReadFile("./contracts/escrow.tenor.wasm")
	if err != nil {
		log.Fatalf("failed to read file: %v", err)
	}
	evaluator, err = tenor.NewEvaluatorFromBytes(ctx, wasmBytes)
	if err != nil {
		log.Fatalf("failed to load contract: %v", err)
	}
	defer evaluator.Close(ctx)

	fmt.Println("Contract loaded successfully")
}

Evaluating a Contract

go
result, err := evaluator.Evaluate(ctx, tenor.EvalInput{
	Facts: map[string]any{
		"payment_received": true,
		"payment_amount":   "5000.00",
		"customer_tier":    "premium",
	},
	Entities: map[string]map[string]string{
		"Order":   {"order_001": "pending"},
		"Payment": {"pay_001": "cleared"},
	},
})
if err != nil {
	log.Fatalf("evaluation failed: %v", err)
}

for name, verdict := range result.Verdicts {
	fmt.Printf("%s: value=%v stratum=%d\n", name, verdict.Value, verdict.Stratum)
}
// payment_ok: value=true stratum=0
// eligible_for_release: value=true stratum=1

fmt.Printf("evaluation took %v\n", result.EvaluationTime)
// evaluation took 412µs

Computing the Action Space

go
actions, err := evaluator.ActionSpace(ctx, tenor.EvalInput{
	Facts: map[string]any{
		"payment_received": true,
		"payment_amount":   "5000.00",
		"customer_tier":    "premium",
	},
	Entities: map[string]map[string]string{
		"Order":   {"order_001": "pending"},
		"Payment": {"pay_001": "cleared"},
	},
})
if err != nil {
	log.Fatalf("action space computation failed: %v", err)
}

for persona, personaActions := range actions {
	fmt.Printf("--- %s ---\n", persona)
	for _, action := range personaActions {
		fmt.Printf("  %s (precondition_met=%v)\n", action.Operation, action.PreconditionMet)
		for _, e := range action.EntitiesAffected {
			fmt.Printf("    %s:%s %s -> %s\n", e.Entity, e.Instance, e.FromState, e.ToState)
		}
	}
}
// --- escrow_agent ---
//   release_funds (precondition_met=true)
//     Order:order_001 pending -> released
// --- buyer ---
//   dispute_transaction (precondition_met=true)
//     Order:order_001 pending -> disputed
// --- seller ---

Action Space for a Single Persona

go
buyerActions, err := evaluator.ActionSpaceForPersona(ctx, tenor.EvalInput{
	Facts: map[string]any{
		"payment_received": true,
		"payment_amount":   "5000.00",
	},
	Entities: map[string]map[string]string{
		"Order": {"order_001": "pending"},
	},
}, "buyer")
if err != nil {
	log.Fatalf("action space computation failed: %v", err)
}

for _, action := range buyerActions {
	fmt.Printf("%s: precondition_met=%v\n", action.Operation, action.PreconditionMet)
}
// dispute_transaction: precondition_met=true

Contract Metadata

go
meta := evaluator.Metadata()

fmt.Println("Entities:")
for _, e := range meta.Entities {
	fmt.Printf("  %s: states=%v initial=%s\n", e.Name, e.States, e.Initial)
}
// Entities:
//   Order: states=[pending approved disputed released] initial=pending
//   Payment: states=[held released refunded] initial=held

fmt.Println("Personas:", meta.Personas)
// Personas: [escrow_agent buyer seller]

fmt.Println("Facts:")
for _, f := range meta.Facts {
	fmt.Printf("  %s: %s\n", f.Name, f.Type)
}
// Facts:
//   payment_received: Bool
//   payment_amount: Money
//   customer_tier: Text

fmt.Println("Contract hash:", meta.ContractHash)
// Contract hash: sha256:9f4a2b...

Error Handling

go
import "errors"

result, err := evaluator.Evaluate(ctx, tenor.EvalInput{
	Facts: map[string]any{
		"payment_received": "yes", // Wrong type: expected Bool
	},
	Entities: map[string]map[string]string{},
})

var typeErr *tenor.TypeError
if errors.As(err, &typeErr) {
	fmt.Printf("Type error on fact %q: expected %s, got %s\n",
		typeErr.Fact, typeErr.ExpectedType, typeErr.ReceivedType)
	// Type error on fact "payment_received": expected Bool, got Text
}

var undeclaredErr *tenor.UndeclaredFactError
if errors.As(err, &undeclaredErr) {
	fmt.Printf("Undeclared fact: %q\n", undeclaredErr.Fact)
	// Undeclared fact: "nonexistent_fact"
}

Full Working Example: Contract Testing

go
package escrow_test

import (
	"context"
	"testing"

	tenor "github.com/riverline-labs/tenor/sdks/go"
)

var evaluator *tenor.Evaluator

func TestMain(m *testing.M) {
	ctx := context.Background()
	var err error
	evaluator, err = tenor.NewEvaluator(ctx, "./testdata/escrow.tenor.wasm")
	if err != nil {
		panic("failed to load contract: " + err.Error())
	}
	defer evaluator.Close(ctx)
	m.Run()
}

func TestPaymentVerdictFiresWhenPaymentReceived(t *testing.T) {
	ctx := context.Background()
	result, err := evaluator.Evaluate(ctx, tenor.EvalInput{
		Facts: map[string]any{
			"payment_received": true,
			"payment_amount":   "5000.00",
		},
		Entities: map[string]map[string]string{
			"Order": {"order_001": "pending"},
		},
	})
	if err != nil {
		t.Fatalf("evaluation failed: %v", err)
	}

	v, ok := result.Verdicts["payment_ok"]
	if !ok {
		t.Fatal("expected payment_ok verdict to be present")
	}
	if v.Value != true {
		t.Errorf("expected payment_ok=true, got %v", v.Value)
	}
	if v.Stratum != 0 {
		t.Errorf("expected stratum 0, got %d", v.Stratum)
	}
}

func TestPaymentVerdictAbsentWithoutPayment(t *testing.T) {
	ctx := context.Background()
	result, err := evaluator.Evaluate(ctx, tenor.EvalInput{
		Facts: map[string]any{
			"payment_received": false,
			"payment_amount":   "0.00",
		},
		Entities: map[string]map[string]string{
			"Order": {"order_001": "pending"},
		},
	})
	if err != nil {
		t.Fatalf("evaluation failed: %v", err)
	}

	if _, ok := result.Verdicts["payment_ok"]; ok {
		t.Error("expected payment_ok verdict to be absent when payment not received")
	}
}

func TestEscrowAgentCanRelease(t *testing.T) {
	ctx := context.Background()
	actions, err := evaluator.ActionSpaceForPersona(ctx, tenor.EvalInput{
		Facts: map[string]any{
			"payment_received": true,
			"payment_amount":   "5000.00",
			"release_approved": true,
		},
		Entities: map[string]map[string]string{
			"Order": {"order_001": "pending"},
		},
	}, "escrow_agent")
	if err != nil {
		t.Fatalf("action space failed: %v", err)
	}

	found := false
	for _, a := range actions {
		if a.Operation == "release_funds" && a.PreconditionMet {
			found = true
			break
		}
	}
	if !found {
		t.Error("expected escrow_agent to have release_funds available")
	}
}

func TestBuyerCannotRelease(t *testing.T) {
	ctx := context.Background()
	actions, err := evaluator.ActionSpaceForPersona(ctx, tenor.EvalInput{
		Facts: map[string]any{
			"payment_received": true,
			"payment_amount":   "5000.00",
			"release_approved": true,
		},
		Entities: map[string]map[string]string{
			"Order": {"order_001": "pending"},
		},
	}, "buyer")
	if err != nil {
		t.Fatalf("action space failed: %v", err)
	}

	for _, a := range actions {
		if a.Operation == "release_funds" {
			t.Error("buyer should not have release_funds in action space")
		}
	}
}

func TestTerminalStateHasNoActions(t *testing.T) {
	ctx := context.Background()
	actions, err := evaluator.ActionSpace(ctx, tenor.EvalInput{
		Facts: map[string]any{
			"payment_received": true,
			"payment_amount":   "5000.00",
		},
		Entities: map[string]map[string]string{
			"Order": {"order_001": "released"}, // terminal state
		},
	})
	if err != nil {
		t.Fatalf("action space failed: %v", err)
	}

	for persona, personaActions := range actions {
		for _, a := range personaActions {
			for _, e := range a.EntitiesAffected {
				if e.Entity == "Order" {
					t.Errorf("%s has action %s on terminal Order", persona, a.Operation)
				}
			}
		}
	}
}

func TestTypeErrorOnWrongFactType(t *testing.T) {
	ctx := context.Background()
	_, err := evaluator.Evaluate(ctx, tenor.EvalInput{
		Facts: map[string]any{
			"payment_received": "yes", // Wrong type
		},
		Entities: map[string]map[string]string{},
	})
	if err == nil {
		t.Fatal("expected error for wrong fact type")
	}

	var typeErr *tenor.TypeError
	if !errors.As(err, &typeErr) {
		t.Fatalf("expected TypeError, got %T: %v", err, err)
	}
	if typeErr.Fact != "payment_received" {
		t.Errorf("expected fact 'payment_received', got %q", typeErr.Fact)
	}
	if typeErr.ExpectedType != "Bool" {
		t.Errorf("expected type Bool, got %q", typeErr.ExpectedType)
	}
}

Performance Notes

The wazero runtime compiles WASM to native code on first load (typically 10-50ms depending on contract size). Subsequent evaluations against the same loaded evaluator take microseconds. For high-throughput services, load the evaluator once at startup and reuse it across requests.

go
// Load once at startup
evaluator, _ := tenor.NewEvaluator(ctx, "./contracts/escrow.tenor.wasm")

// Reuse across goroutines — evaluator is safe for concurrent use
http.HandleFunc("/evaluate", func(w http.ResponseWriter, r *http.Request) {
    result, err := evaluator.Evaluate(r.Context(), input)
    // ...
})

The evaluator is safe for concurrent use from multiple goroutines. No mutex is needed. Each call gets its own WASM execution context within the wazero runtime.