actorlayer

package module
v0.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 27, 2026 License: MIT Imports: 7 Imported by: 0

README

go-actorlayer

Test Lint Latest release

go-actorlayer is a small Go library for message-driven application code.

Use it when your system has things like:

  • chat or user sessions;
  • background jobs;
  • multi-step workflows;
  • retries, delays, and dead letters;
  • work that should continue after the original request is gone.

It gives you a small execution core without taking ownership of your broker, headers, stream names, storage, or application semantics.

github.com/baldaworks/go-actorlayer

Documentation

How to think about it

You send work as messages.

Each message has:

  • an address, like session:demo or job:task-42;
  • a payload;
  • delivery behavior controlled by the transport.

A handler is registered for an address pattern. The runtime receives a message, finds the right handler, runs it, and lets the transport decide whether that message is acknowledged, retried, or dead-lettered.

You do not need to know actor theory to use it. In this library, an actor is just a message handler with an address.

sequenceDiagram
    participant App as Producer or app code
    participant Transport as Transport
    participant Runtime as Runtime
    participant Registry as Registry
    participant Handler as Actor handler

    App->>Transport: Dispatch Envelope(to=session:demo)
    Transport->>Runtime: Deliver Envelope
    Runtime->>Registry: Resolve session:demo
    Registry-->>Runtime: Matching handler
    Runtime->>Handler: Handle(ctx, envelope)

    alt Handler succeeds
        Handler-->>Runtime: nil
        Runtime->>Transport: Ack(delivery)
    else Handler returns retryable error
        Handler-->>Runtime: transient or external delivery error
        Runtime->>Transport: Retry(delivery, delay)
    else Handler returns terminal error
        Handler-->>Runtime: policy, permanent, or decode error
        Runtime->>Transport: DeadLetter(delivery, reason)
    end

When to use it

go-actorlayer is a good fit when you want:

  • durable envelopes for work;
  • explicit routing to handlers;
  • exact or wildcard handler resolution;
  • per-lane serialization;
  • transport-owned Ack, Retry, and DeadLetter;
  • a runtime core that you can adapt to your own queue or broker.

It works especially well for AI systems and agents, where one conversation, task, or workflow may span multiple steps, retries, tool calls, cancellations, or background runs.

It is not trying to be:

  • a full application framework;
  • a workflow product;
  • a queue or broker;
  • a persistence layer;
  • a ready-made agent framework.

Install

go get github.com/baldaworks/go-actorlayer@latest

Quick start

This is the smallest end-to-end path:

  1. define a handler;
  2. register it;
  3. start the runtime;
  4. dispatch a message.
package main

import (
	"context"
	"fmt"

	"github.com/baldaworks/go-actorlayer"
	"github.com/baldaworks/go-actorlayer/dispatch"
	"github.com/baldaworks/go-actorlayer/engine"
	"github.com/baldaworks/go-actorlayer/transport/memory"
)

type welcomePayload struct {
	Name string `json:"name"`
}

type welcomeActor struct {
	done chan struct{}
}

func (a welcomeActor) Address() string { return actorlayer.WildcardAddress("session") }

func (a welcomeActor) Handle(_ context.Context, env actorlayer.Envelope) error {
	var payload welcomePayload
	if err := actorlayer.UnmarshalPayload(env.Payload, &payload); err != nil {
		return err
	}
	fmt.Printf("welcome %s\n", payload.Name)
	close(a.done)
	return nil
}

func main() {
	bus := memory.New(1)
	registry := dispatch.NewMemoryRegistry()
	done := make(chan struct{})
	_ = registry.Register(welcomeActor{done: done})

	runtime, _ := engine.NewDispatchRuntime(engine.RuntimeConfig{
		Registry:  registry,
		AddressOf: func(env engine.Envelope) (string, error) { return env.To.String() },
		Retry: engine.RetryPolicy{
			IsRetryable: actorlayer.IsRetryableError,
			Backoff:     actorlayer.RetryDelay,
		},
	})

	ctx := context.Background()
	runDone := make(chan error, 1)
	go func() {
		runDone <- runtime.Run(ctx, bus)
	}()

	payload, _ := actorlayer.MarshalPayload(welcomePayload{Name: "ada"})
	_, _ = bus.Dispatch(ctx, actorlayer.Envelope{
		ID:        "env-1",
		Namespace: "example.command",
		Kind:      "welcome",
		From:      actorlayer.SystemAddress("example"),
		To:        actorlayer.ActorAddress{Target: "session", Key: "demo"},
		Payload:   payload,
	})

	<-done
	_ = bus.Drain(ctx)
	<-runDone
}

Output:

welcome ada

The same example is covered by example_test.go.

Packages

  • actorlayer: envelope types, addresses, payload helpers, codecs, errors, retry helpers.
  • dispatch: registration and lookup for handlers.
  • engine: the runtime that executes deliveries.
  • transport: interfaces for dispatching, consuming, publishing events, and draining.
  • transport/memory: an in-memory transport for tests and examples.

Dependency direction:

flowchart LR
    Memory["transport/memory"] --> Engine["engine"]
    Memory --> Transport["transport"]
    Engine --> Dispatch["dispatch"]
    Engine --> Core["actorlayer"]
    Dispatch --> Core
    Transport --> Core

Mental model

flowchart LR
    Envelope["Envelope"] --> Delivery["Delivery"]
    Delivery --> Runtime["Runtime"]
    Runtime --> Registry["Registry lookup"]
    Registry --> Handler["Actor.Handle"]
    Handler --> Runtime
    Runtime --> Settlement["Ack / Retry / DeadLetter"]
  • an Envelope is the unit of work;
  • a Delivery is the transport wrapper around that work;
  • the Runtime validates the message and decides where it runs;
  • the Registry finds the right handler;
  • the transport decides what Ack, Retry, and DeadLetter mean.

That split is deliberate:

  • the runtime owns execution flow;
  • the transport owns broker behavior;
  • the application owns business logic, headers, persistence, and policy.

Envelope and payloads

An envelope carries:

  • identity: ID, CorrelationID, CausationID, DedupeKey;
  • addressing: From, To, optional ReportTo;
  • execution hints: Priority, Attempt, MaxAttempts, NotBefore, ExpiresAt;
  • payload: Payload;
  • generic metadata: Meta.

Payload is format-neutral:

  • Encoding identifies the codec, for example json;
  • Data contains the raw bytes.

In most code you should use:

  • MarshalPayload(v) to build a payload with the default JSON codec;
  • UnmarshalPayload(payload, &dst) to decode it.

If you need custom encodings, use:

  • Codec
  • CodecRegistry
  • NewCodecRegistry
  • MarshalPayloadWithCodec
  • UnmarshalPayloadWithRegistry
  • EncodeEnvelopeWithRegistry
  • DecodeEnvelopeWithRegistry

The built-in default is JSONCodec. On the wire, JSON payloads keep the legacy payload_json projection for compatibility, while non-JSON payloads use payload_encoding plus base64-encoded payload_data.

Custom encodings are first-class in the codec-aware marshal, unmarshal, and envelope encode/decode paths above. The default helper path still uses the built-in registry, so raw payload construction through NewPayload only accepts encodings registered there.

Addressing

Addresses look like target:key.

Examples:

  • session:demo
  • job:task-42
  • delivery:user-42

Helpful rules:

  • ActorAddress.String() validates concrete addresses.
  • WildcardAddress("session") produces session:*.
  • dispatch.MemoryRegistry resolves exact matches first, then target wildcard.

Registry and engine dispatch paths normalize addresses case-insensitively.

Errors and retries

The library distinguishes:

  • transient
  • policy
  • permanent
  • decode
  • external_delivery

Use:

  • actorlayer.TransientError(err)
  • actorlayer.PolicyError(err)
  • actorlayer.PermanentError(err)
  • actorlayer.DecodeError(err)
  • actorlayer.ExternalDeliveryError(err)

Most hosts should use actorlayer.IsRetryableError as the default rule:

  • retryable: transient, external delivery;
  • terminal: policy, permanent, decode, actor not found.

engine.RetryPolicy lets you define:

  • which errors retry;
  • how backoff works;
  • when retries are exhausted.

Extension points

To integrate with a real queue or broker, implement some or all of:

  • transport.Dispatcher
  • engine.Source
  • engine.Delivery
  • transport.EventPublisher
  • transport.EventConsumer
  • transport.Drainer

You can also replace dispatch.Registry if process-local in-memory registration is not enough.

Out of scope

This module intentionally does not define:

  • broker subjects, headers, streams, or queue names;
  • database schemas or read models;
  • product concepts such as users, sessions, goals, or channels;
  • config loading or application lifecycle wiring;
  • observability policy.

Contributing

See CONTRIBUTING.md.

Release

  • CI: .github/workflows/test.yml
  • Lint: .github/workflows/lint.yml
  • GitHub release tags: .github/workflows/release.yml
  • Maintainer checklist: RELEASING.md

Documentation

Overview

Package actorlayer provides the core types for a small transport-agnostic actor runtime.

The package defines the durable Envelope model, actor addresses, payload helpers, error classification, and retry helpers used by the higher-level dispatch, engine, and transport packages.

Package layout:

  • actorlayer: envelope model, addresses, payload helpers, errors, retry
  • dispatch: actor registration and address resolution
  • engine: delivery execution, lane serialization, settlement hooks
  • transport: dispatch, event, and drain contracts
  • transport/memory: in-memory transport for tests, examples, and local use

Payload carries an explicit encoding plus raw bytes. Use MarshalPayload and UnmarshalPayload to encode and decode typed payloads. The default helpers use the built-in JSON codec unless a codec-aware helper is used explicitly. ReportTo is optional, but when present it must be a valid actor address.

Example (EndToEnd)
package main

import (
	"context"
	"fmt"

	"github.com/baldaworks/go-actorlayer"
	"github.com/baldaworks/go-actorlayer/dispatch"
	"github.com/baldaworks/go-actorlayer/engine"
	"github.com/baldaworks/go-actorlayer/transport/memory"
)

type welcomePayload struct {
	Name string `json:"name"`
}

type welcomeActor struct {
	done chan struct{}
}

func (a welcomeActor) Address() string { return actorlayer.WildcardAddress("session") }

func (a welcomeActor) Handle(_ context.Context, env actorlayer.Envelope) error {
	var payload welcomePayload
	if err := actorlayer.UnmarshalPayload(env.Payload, &payload); err != nil {
		return err
	}
	fmt.Printf("welcome %s\n", payload.Name)
	close(a.done)
	return nil
}

func main() {
	bus := memory.New(1)
	registry := dispatch.NewMemoryRegistry()
	done := make(chan struct{})
	_ = registry.Register(welcomeActor{done: done})

	runtime, _ := engine.NewDispatchRuntime(engine.RuntimeConfig{
		Registry:  registry,
		AddressOf: func(env engine.Envelope) (string, error) { return env.To.String() },
		Retry: engine.RetryPolicy{
			IsRetryable: actorlayer.IsRetryableError,
			Backoff:     actorlayer.RetryDelay,
		},
	})

	ctx := context.Background()
	runDone := make(chan error, 1)
	go func() {
		runDone <- runtime.Run(ctx, bus)
	}()

	payload, _ := actorlayer.MarshalPayload(welcomePayload{Name: "ada"})
	_, _ = bus.Dispatch(ctx, actorlayer.Envelope{
		ID:        "env-1",
		Namespace: "example.command",
		Kind:      "welcome",
		From:      actorlayer.SystemAddress("example"),
		To:        actorlayer.ActorAddress{Target: "session", Key: "demo"},
		Payload:   payload,
	})

	<-done
	_ = bus.Drain(ctx)
	<-runDone

}
Output:
welcome ada

Index

Examples

Constants

View Source
const EncodingJSON = "json"

EncodingJSON identifies the built-in JSON payload codec.

Variables

View Source
var ErrActorNotFound = errors.New("actor not found")

ErrActorNotFound reports that an actor address could not be resolved.

Functions

func DecodeError

func DecodeError(err error) error

DecodeError marks err as malformed input.

func DedupeKeyOrID

func DedupeKeyOrID(env Envelope) string

DedupeKeyOrID returns the explicit dedupe key, or the envelope ID when no dedupe key is set.

func EncodeEnvelope

func EncodeEnvelope(e Envelope) (string, error)

EncodeEnvelope validates and marshals an envelope using the default JSON-based wire projection.

func EncodeEnvelopeWithRegistry

func EncodeEnvelopeWithRegistry(e Envelope, reg CodecRegistry) (string, error)

EncodeEnvelopeWithRegistry validates and marshals an envelope with the provided registry.

func ExternalDeliveryError

func ExternalDeliveryError(err error) error

ExternalDeliveryError marks err as retryable delivery infrastructure failure.

func IsRetryableError

func IsRetryableError(err error) bool

IsRetryableError reports whether err should be retried by default.

func PermanentError

func PermanentError(err error) error

PermanentError marks err as non-retryable permanent failure.

func PolicyError

func PolicyError(err error) error

PolicyError marks err as non-retryable policy failure.

func RetryDelay

func RetryDelay(attempt int) time.Duration

RetryDelay returns an exponential retry delay with bounded jitter.

func RetryExhausted

func RetryExhausted(attempt int, maxAttempts int) bool

RetryExhausted reports whether the current one-based attempt has reached the configured max attempts.

func TransientError

func TransientError(err error) error

TransientError marks err as retryable.

func UnmarshalPayload

func UnmarshalPayload(p Payload, dst any) error

UnmarshalPayload unmarshals an actor payload into dst with the default registry.

func UnmarshalPayloadWithRegistry

func UnmarshalPayloadWithRegistry(reg CodecRegistry, p Payload, dst any) error

UnmarshalPayloadWithRegistry unmarshals payload content using the provided registry.

func WildcardAddress

func WildcardAddress(target string) string

WildcardAddress returns the normalized registry address for all keys in a target, such as "session:*".

Types

type ActorAddress

type ActorAddress struct {
	Target string `json:"target"`
	Key    string `json:"key"`
}

ActorAddress identifies an actor target and concrete key.

func SystemAddress

func SystemAddress(key string) ActorAddress

SystemAddress returns an address in the reserved "system" target.

func (ActorAddress) String

func (a ActorAddress) String() (string, error)

String returns the normalized full actor address.

type ActorError

type ActorError struct {
	Kind ErrorKind
	Err  error
}

ActorError wraps an error with an actorlayer classification.

func (*ActorError) Error

func (e *ActorError) Error() string

func (*ActorError) Unwrap

func (e *ActorError) Unwrap() error

type Codec

type Codec interface {
	Encoding() string
	Marshal(v any) ([]byte, error)
	Unmarshal(data []byte, v any) error
	Validate(data []byte) error
}

Codec marshals and unmarshals actor payload values for one encoding.

var JSONCodec Codec = jsonCodec{}

JSONCodec is the default actorlayer codec and preserves current wire/storage compatibility.

type CodecRegistry

type CodecRegistry interface {
	Lookup(encoding string) (Codec, bool)
}

CodecRegistry resolves codecs by encoding name.

func DefaultCodecRegistry

func DefaultCodecRegistry() CodecRegistry

DefaultCodecRegistry returns the built-in codec registry.

func NewCodecRegistry

func NewCodecRegistry(codecs ...Codec) CodecRegistry

NewCodecRegistry builds a registry from the provided codecs.

type Envelope

type Envelope struct {
	ID            string
	Namespace     string
	Kind          string
	From          ActorAddress
	To            ActorAddress
	CorrelationID string
	CausationID   string
	Priority      int
	DedupeKey     string
	Attempt       int
	MaxAttempts   int
	NotBefore     time.Time
	ExpiresAt     time.Time
	Payload       Payload
	Meta          map[string]string
	ReportTo      *ActorAddress
}

Envelope is the durable actor transport unit.

func DecodeEnvelope

func DecodeEnvelope(raw string) (Envelope, error)

DecodeEnvelope unmarshals and validates an envelope JSON string.

func DecodeEnvelopeWithRegistry

func DecodeEnvelopeWithRegistry(raw string, reg CodecRegistry) (Envelope, error)

DecodeEnvelopeWithRegistry unmarshals and validates an envelope string with the provided registry.

func (Envelope) Validate

func (e Envelope) Validate() error

Validate verifies the envelope fields required by actorlayer runtimes and transports.

func (Envelope) ValidateWithRegistry

func (e Envelope) ValidateWithRegistry(reg CodecRegistry) error

ValidateWithRegistry verifies the envelope with the provided registry.

type ErrorKind

type ErrorKind string

ErrorKind classifies actor and delivery failures for retry decisions.

const (
	// ErrorKindTransient marks a retryable temporary failure.
	ErrorKindTransient ErrorKind = "transient"
	// ErrorKindPolicy marks a non-retryable policy or authorization failure.
	ErrorKindPolicy ErrorKind = "policy"
	// ErrorKindPermanent marks a non-retryable permanent actor failure.
	ErrorKindPermanent ErrorKind = "permanent"
	// ErrorKindDecode marks malformed payload or envelope input.
	ErrorKindDecode ErrorKind = "decode"
	// ErrorKindExternalDelivery marks retryable infrastructure delivery failure.
	ErrorKindExternalDelivery ErrorKind = "external_delivery"
)

func ClassifyError

func ClassifyError(err error) ErrorKind

ClassifyError returns the actorlayer error kind for err.

Unclassified non-nil errors are treated as transient to preserve retry by default.

type Payload

type Payload struct {
	Encoding string
	Data     []byte
}

Payload is the format-neutral envelope body.

func MarshalPayload

func MarshalPayload(v any) (Payload, error)

MarshalPayload marshals a typed actor payload with the default JSON codec.

func MarshalPayloadWithCodec

func MarshalPayloadWithCodec(codec Codec, v any) (Payload, error)

MarshalPayloadWithCodec marshals a typed actor payload using the provided codec.

func NewPayload

func NewPayload(encoding string, data []byte) (Payload, error)

NewPayload validates and normalizes a payload value.

func (Payload) String

func (p Payload) String() string

String returns payload data as a string for text-based transports and legacy compatibility paths.

func (Payload) Validate

func (p Payload) Validate() error

Validate verifies the payload shape and codec-level validity.

func (Payload) ValidateWithRegistry

func (p Payload) ValidateWithRegistry(reg CodecRegistry) error

ValidateWithRegistry verifies the payload using the provided registry.

Directories

Path Synopsis
Package dispatch provides actor registration and address resolution helpers.
Package dispatch provides actor registration and address resolution helpers.
Package engine provides transport-agnostic actor delivery execution.
Package engine provides transport-agnostic actor delivery execution.
Package transport defines actorlayer transport interfaces.
Package transport defines actorlayer transport interfaces.
memory
Package memory provides an in-memory actorlayer transport implementation.
Package memory provides an in-memory actorlayer transport implementation.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL