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"
}

TenorClient — Remote HTTP API

TenorClient calls the Tenor platform for evaluation and execution against managed state.

Constructor

go
client, err := tenor.NewClient(tenor.ClientConfig{
	Org:     "acme",
	APIKey:  "tk_live_9f4a2b3c4d5e6f7a8b9c0d1e2f3a4b5c",
	// Optional overrides:
	BaseURL: "https://api.tenor.run", // default
	Timeout: 10 * time.Second,        // default 10s
	Retries: 3,                        // default 3
})
if err != nil {
	log.Fatalf("failed to create client: %v", err)
}

Evaluate

go
result, err := client.Evaluate(ctx, "escrow", tenor.EvalInput{
	Facts: map[string]any{
		"payment_received": true,
		"payment_amount":   "5000.00",
	},
})
if err != nil {
	log.Fatalf("evaluation failed: %v", err)
}

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

fmt.Println("Request ID:", result.RequestID)
// Request ID: req_20260215_abc123

Execute a Flow

go
execution, err := client.ExecuteFlow(ctx, "escrow", "release_flow", tenor.ExecuteInput{
	Persona: "escrow_agent",
	Facts: map[string]any{
		"release_approved": true,
		"release_reason":   "Goods received and inspected",
	},
	EntityBindings: map[string]string{
		"Order":   "order_001",
		"Payment": "pay_001",
	},
	IdempotencyKey: "idem_20260215_001",
})
if err != nil {
	log.Fatalf("execution failed: %v", err)
}

fmt.Println("Status:", execution.Status)
// Status: completed

for _, step := range execution.StepsExecuted {
	fmt.Printf("Step %s (%s): %s\n", step.Step, step.Operation, step.Result)
	for _, t := range step.EntityTransitions {
		fmt.Printf("  %s:%s %s -> %s\n", t.Entity, t.Instance, t.FromState, t.ToState)
	}
}
// Step verify_approval (verify_release): success
//   Order:order_001 pending -> approved
// Step release_payment (release_funds): success
//   Payment:pay_001 held -> released

fmt.Println("Provenance chain:", execution.ProvenanceChainID)
// Provenance chain: chain_20260215_001

Simulate

go
sim, err := client.Simulate(ctx, "escrow", tenor.SimulateInput{
	Flow:    "release_flow",
	Persona: "escrow_agent",
	Facts: map[string]any{
		"release_approved": true,
	},
	EntityBindings: map[string]string{
		"Order":   "order_001",
		"Payment": "pay_001",
	},
})
if err != nil {
	log.Fatalf("simulation failed: %v", err)
}

fmt.Println("Status:", sim.Status)
// Status: would_succeed

for _, step := range sim.ProjectedSteps {
	fmt.Printf("%s: would_succeed=%v\n", step.Step, step.WouldSucceed)
}
// verify_approval: would_succeed=true
// release_payment: would_succeed=true

Error Handling

go
import "errors"

execution, err := client.ExecuteFlow(ctx, "escrow", "release_flow", tenor.ExecuteInput{
	Persona: "buyer", // Not authorized for release
	Facts:   map[string]any{"release_approved": true},
	EntityBindings: map[string]string{"Order": "order_001"},
})

var forbiddenErr *tenor.ForbiddenError
if errors.As(err, &forbiddenErr) {
	fmt.Printf("Persona %q cannot perform %q\n", forbiddenErr.Persona, forbiddenErr.Operation)
	fmt.Printf("Allowed personas: %v\n", forbiddenErr.AllowedPersonas)
	// Persona "buyer" cannot perform "release_funds"
	// Allowed personas: [escrow_agent]
}

var precondErr *tenor.PreconditionError
if errors.As(err, &precondErr) {
	fmt.Printf("Precondition failed: %s\n", precondErr.ErrorMessage)
	// Precondition failed: Release conditions not met.
}

var conflictErr *tenor.ConflictError
if errors.As(err, &conflictErr) {
	fmt.Printf("Conflict: %s:%s expected %s, found %s\n",
		conflictErr.Entity, conflictErr.Instance,
		conflictErr.ExpectedState, conflictErr.ActualState)
	// Conflict: Order:order_001 expected pending, found disputed
}

var rateLimitErr *tenor.RateLimitError
if errors.As(err, &rateLimitErr) {
	fmt.Printf("Rate limited, retry after %v\n", rateLimitErr.RetryAfter)
	// Rate limited, retry after 1.2s
	// SDK retries automatically up to Retries times
}

Full Working Example: HTTP Service with chi

A complete example using the Go SDK with chi to build an escrow management service:

go
package main

import (
	"context"
	"encoding/json"
	"errors"
	"log"
	"net/http"
	"os"
	"time"

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

var client *tenor.Client

func main() {
	var err error
	client, err = tenor.NewClient(tenor.ClientConfig{
		Org:    "acme",
		APIKey: os.Getenv("TENOR_API_KEY"),
	})
	if err != nil {
		log.Fatalf("failed to create Tenor client: %v", err)
	}

	r := chi.NewRouter()
	r.Use(middleware.Logger)
	r.Use(middleware.Recoverer)
	r.Use(middleware.Timeout(30 * time.Second))

	r.Get("/orders/{orderID}/actions", handleGetActions)
	r.Post("/orders/{orderID}/release", handleRelease)
	r.Post("/orders/{orderID}/dispute", handleDispute)

	log.Println("Escrow service listening on :8080")
	log.Fatal(http.ListenAndServe(":8080", r))
}

func handleGetActions(w http.ResponseWriter, r *http.Request) {
	persona := r.Header.Get("X-Persona")
	if persona == "" {
		http.Error(w, `{"error":"Missing X-Persona header"}`, http.StatusBadRequest)
		return
	}

	actions, err := client.Actions(r.Context(), "escrow", persona)
	if err != nil {
		var forbiddenErr *tenor.ForbiddenError
		if errors.As(err, &forbiddenErr) {
			http.Error(w, `{"error":"Unknown persona"}`, http.StatusForbidden)
			return
		}
		http.Error(w, `{"error":"Internal error"}`, http.StatusInternalServerError)
		return
	}

	type actionSummary struct {
		Operation string `json:"operation"`
		Available bool   `json:"available"`
	}
	resp := struct {
		Persona string          `json:"persona"`
		OrderID string          `json:"order_id"`
		Actions []actionSummary `json:"actions"`
	}{
		Persona: persona,
		OrderID: chi.URLParam(r, "orderID"),
	}
	for _, a := range actions {
		resp.Actions = append(resp.Actions, actionSummary{
			Operation: a.Operation,
			Available: a.PreconditionMet,
		})
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(resp)
}

func handleRelease(w http.ResponseWriter, r *http.Request) {
	persona := r.Header.Get("X-Persona")
	orderID := chi.URLParam(r, "orderID")

	var body struct {
		Reason string `json:"reason"`
	}
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		http.Error(w, `{"error":"Invalid request body"}`, http.StatusBadRequest)
		return
	}

	execution, err := client.ExecuteFlow(r.Context(), "escrow", "release_flow", tenor.ExecuteInput{
		Persona: persona,
		Facts: map[string]any{
			"release_approved": true,
			"release_reason":   body.Reason,
		},
		EntityBindings: map[string]string{
			"Order": orderID,
		},
	})
	if err != nil {
		var forbiddenErr *tenor.ForbiddenError
		if errors.As(err, &forbiddenErr) {
			w.WriteHeader(http.StatusForbidden)
			json.NewEncoder(w).Encode(map[string]string{
				"error": forbiddenErr.ErrorMessage,
			})
			return
		}
		var precondErr *tenor.PreconditionError
		if errors.As(err, &precondErr) {
			w.WriteHeader(http.StatusUnprocessableEntity)
			json.NewEncoder(w).Encode(map[string]string{
				"error": precondErr.ErrorMessage,
			})
			return
		}
		http.Error(w, `{"error":"Internal error"}`, http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]any{
		"status":               execution.Status,
		"provenance_chain_id":  execution.ProvenanceChainID,
	})
}

func handleDispute(w http.ResponseWriter, r *http.Request) {
	persona := r.Header.Get("X-Persona")
	orderID := chi.URLParam(r, "orderID")

	var body struct {
		Reason string `json:"reason"`
	}
	if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
		http.Error(w, `{"error":"Invalid request body"}`, http.StatusBadRequest)
		return
	}

	execution, err := client.ExecuteFlow(r.Context(), "escrow", "dispute_flow", tenor.ExecuteInput{
		Persona: persona,
		Facts: map[string]any{
			"dispute_reason": body.Reason,
		},
		EntityBindings: map[string]string{
			"Order": orderID,
		},
	})
	if err != nil {
		var forbiddenErr *tenor.ForbiddenError
		if errors.As(err, &forbiddenErr) {
			w.WriteHeader(http.StatusForbidden)
			json.NewEncoder(w).Encode(map[string]string{
				"error": forbiddenErr.ErrorMessage,
			})
			return
		}
		http.Error(w, `{"error":"Internal error"}`, http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	json.NewEncoder(w).Encode(map[string]any{
		"status":               execution.Status,
		"provenance_chain_id":  execution.ProvenanceChainID,
	})
}

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.