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
go get github.com/riverline-labs/tenor/sdks/goRequires 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
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
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µsComputing the Action Space
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
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=trueContract Metadata
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
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
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
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_abc123Execute a Flow
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_001Simulate
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=trueError Handling
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:
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
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.
// 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.