Borz actor

The actor is the unit. Deterministic. Typed. Four targets.

Each Borz actor is an isolated process with its own memory and message queue. Handlers run one at a time. No shared variables, no locks, no global state. The same source compiles to four targets — native binary, DEMIX microservice, DENSE nanoservice, or Ephernity smart contract. The compiler enforces these properties — they are not conventions.

account.borz spec v0.9.5
msg Deposit:
    amount: i64

msg Withdraw:
    amount: i64

msg Balance: {}

actor Account:
    @persistent var balance: i64 = 0   # survives restarts
    @persistent var owner:   str = ""

    @http(path="/deposit")
    on msg Deposit:
        balance = balance + msg.amount
        response(ok=true, balance=balance)

    @http(path="/withdraw")
    on msg Withdraw:
        if msg.amount > balance:
            response(ok=false, error="insufficient funds")
        else:
            balance = balance - msg.amount
            response(ok=true, balance=balance)

    @http(path="/balance")
    on msg Balance:
        response(balance=balance)
Isolated state

Each actor owns its memory. No actor can read or write another's state directly. Communication happens only through typed messages.

Sequential handlers

Handlers run one at a time per actor. No data races, no deadlocks, no mutexes. Concurrency comes from having many actors, not from threads inside one.

Persistent by default

Mark any state variable @persistent and the compiler wires up save/restore automatically. Actors survive restarts, hot-reloads, and target changes.

Typed messages

Every message is a named struct with typed fields. The compiler rejects a handler that reads a field that does not exist on the message type.

Execution targets

One actor. Four places to run it.

The same .borz source compiles to four distinct targets. No source changes between targets — change the compiler flag, not the program.

--target native

Native binary

A single self-contained Go binary with a built-in HTTP server and CLI. No runtime dependencies. Ideal for local tooling, CI workers, and serverless functions.

Ships today
--target demix

DEMIX microservice

Each actor runs in its own supervised Linux micro-VM. Full network stack, hardware isolation, millisecond cold starts, and rolling hot-reload without dropped connections.

Ships today
--target dense

DENSE nanoservice

Flat ELF binary on bare KVM — no OS, no runtime. ~50 µs p50 dispatch (single no-op handler benchmark), 16 KB binary, 2 MiB memory cap enforced at compile time. Ed25519 receipt per dispatch.

Ships today
--target ephernity

Ephernity smart contract

Deterministic actors compile to smart contracts on the Epher Compute Chain — attested, append-only, and auditable. Write the actor once; the chain enforces invariants at execution.

On roadmap
same source — four different flags
# Compile once — change the target flag, not the program.

borz compile account.borz --target native    # → ./account (Go binary, built-in HTTP + CLI)
borz compile account.borz --target demix     # → DEMIX micro-VM package (supervised lifecycle)
borz compile account.borz --target dense     # → account.elf (flat KVM binary, ~50 µs no-op dispatch)
borz compile account.borz --target ephernity # → account.ecc (Epher Compute Chain contract)
Messages

Messages carry typed data.

A message is a named struct. Fields have types. Actors send messages to each other; handlers pattern-match on message type and access fields directly via msg.field.

message definitions
# scalar fields
msg Greet:
    name: str
    lang: str = "en"   # default value

# sum type (enum with payload)
msg Result:
    Ok:  { value: i64 }
    Err: { reason: str }

# generic message
msg Pair[A, B]:
    first:  A
    second: B

# empty message (trigger / ping)
msg Tick: {}
handler patterns
actor Router:
    # handle multiple message types
    on msg Greet:
        response(reply="hello " + msg.name)

    on msg Tick:
        # no-op tick handler

    # match on sum type variant
    on msg Result:
        match msg:
            Ok(v):
                response(value=v.value)
            Err(e):
                response(error=e.reason)
Decorators

Decorators attach capabilities.

Decorators wire observability, routing, testing, and runtime contracts onto actors and handlers without changing the handler body. The compiler checks them at build time.

routing
@http(path=…)

Expose a handler as an HTTP endpoint. Method is inferred from the message name.

@cli(name=…)

Expose a handler as a CLI subcommand with auto-generated --flag arguments.

@grpc(service=…)

Expose a message handler as a gRPC RPC method.

state
@persistent

Persist a state variable across restarts and hot-reloads. Stored as structured JSON.

@dense(max_state_bytes=N)

Declare the DENSE memory budget for this actor. Enforced at compile time.

testing
@test

Mark a handler as a unit test. Runs in isolation via `borz test`.

@table_test(file=…)

Drive a handler with rows from a CSV or JSON table. One test per row.

@bench

Mark a handler as a benchmark. Reports p50/p99 via `borz bench`.

hot-reload
@migrate_from(v=…)

Declare a state migration handler for rolling hot-reload without data loss.

observability
@trace

Emit a structured trace span on every invocation. Exported as OpenTelemetry.

@metric(name=…)

Increment a named counter or histogram on every invocation.

correctness
@deterministic

Enforce at compile time that this handler calls no wall clocks, floats, or non-deterministic FFI.

@canonical

Enforce byte-identical encoding across all targets. For actors that sign or hash their output.

@meter_budget(cpu_us=N, mem_kb=N)

Declare the per-call resource envelope. The DELIGHT runtime enforces it at dispatch time.

Agentic actors

An actor can call infer — it is still an actor.

When an actor uses the infer statement to embed an LLM call, it becomes an agentic actor. Its core is still deterministic Borz code: it decides what to infer, handles the typed result, and routes the outcome — all in source. It is not a pure LLM agent.

classifier.borz — agentic actor pattern
enum Priority: Urgent | Normal | Low

msg ClassifyTicket:
    text:      str
    ticket_id: str

actor ClassifierActor:
    @persistent var total:   i64      = 0
    @persistent var verdict: Priority = Normal

    on msg ClassifyTicket:
        # infer embeds the LLM call.
        # target must be a typed state var —
        # the result is parsed + validated here.
        infer:
            system: "Classify: Urgent | Normal | Low."
            user:   msg.text
            target: verdict

        total = total + 1

        # deterministic routing after the infer step
        send DispatchActor <- Route(
            ticket_id: msg.ticket_id
            priority:  verdict
        )
The actor controls the LLM call

infer specifies the system prompt, the user input, and the typed target variable. The Borz compiler generates the provider call and validates the response. The actor decides what to do next — the LLM produces one typed value, not an open conversation.

Runs on all four targets

An agentic actor compiles to any of the four targets — native, DEMIX, DENSE, or Ephernity — without changing the source. On DENSE, the infer call exits the guest to reach the LLM host and returns a typed value; the guest resumes deterministically after. DENSE constraints (flat-ELF, memory cap, Ed25519 receipt) still apply.

Different from a pure agent

A Borz agent is the LLM — a persona that processes an entire message as an LLM call. An agentic actor is Borz code that invokes a single typed LLM step. See the agent construct for the distinction.

Language reference

Read the full spec.

The language reference covers actors, messages, traits, generics, expressions, imports, and the full decorator surface in detail.