Two kinds. Not the same thing.
Borz has two distinct first-class constructs for agentic work.
agent is a pure LLM persona —
it runs on a model host such as Claude or ollama, it is inference.
actor is deterministic compiled code
with four compile targets — it may call infer,
but it is still an actor. They share one typed message bus.
agent ReviewAssistant:
model: claude:sonnet
memory: rolling(turns: 20)
system: """
You are a senior code reviewer.
Be direct. Flag correctness, then
style. Include line references.
"""
tools:
- CodeRepo.ReadFile
- CodeRepo.SearchSymbol
on msg ReviewPR:
diff: str
title: str msg GatePR:
pr_id: str
status: str
actor PRGateActor:
@persistent var approved: i64 = 0
@persistent var rejected: i64 = 0
@http(path="/gate")
on msg GatePR:
if msg.status == "approved":
approved = approved + 1
send MergeQueue <- Enqueue(pr_id=msg.pr_id)
else:
rejected = rejected + 1
send NotifyActor <- Alert(pr_id=msg.pr_id) An agent IS the LLM.
An agent definition
is a first-class Borz source construct — written in a .borz file,
managed by the compiler, versioned with your code.
But it does not lower to any binary target. It is a pure LLM agent:
a persona, a model requirement, a toolset, and a typed message interface.
The whole agent is inference — it does not "call" an LLM, it runs as one.
You declare which model and host the agent mandates —
claude:opus,
local:ollama:gemma4:12b —
and the Clan manager routes to it at runtime.
The agent definition is a contract the Clan can check and enforce.
Name, kind, system behaviour written as inline prose. The compiler validates it is present.
Required LLM host and model — checked by the Clan at dispatch time, not at compile time.
The agent declares which message types it handles. The compiler rejects callers that send the wrong shape.
An agent may also accept the generic Prompt message type for open-ended input without a schema.
A declared list of tools the agent is permitted to call. The Clan enforces the list as a policy cap.
Rolling-window or persistent conversation memory. Declared in source; managed by the Clan runtime.
# AnomalyNarrator runs on a local ollama host.
# It is NOT a DENSE actor. It does not compile
# to any binary. It is a pure LLM persona with
# a typed input contract.
agent AnomalyNarrator:
model: local:ollama:gemma4:12b
memory: rolling(turns: 5)
system: """
You are a financial anomaly analyst.
Given a transaction record and a risk
score, write a concise two-sentence
narrative explaining the anomaly and
recommending an action (block, hold,
or approve).
"""
tools:
- MerchantDB.Lookup
- BlacklistDB.Check
on msg NarrateAnomaly:
tx_id: str
amount_eur: i64
merchant: str
risk_score: f64
An agent definition declares a host model, not a compile target.
DENSE constraints (@dense, memory caps, flat-ELF lowering)
do not apply. The compiled artifact is a JSON manifest the Clan runtime provisions.
# TriageActor is an ACTOR, not an agent.
# It compiles to native, DEMIX, DENSE, or Ephernity.
# It calls infer — which makes it "agentic" —
# but its core is deterministic Borz code.
msg TriageTicket:
ticket_id: str
text: str
msg Stats: {}
actor TriageActor:
@persistent var processed: i64 = 0
@persistent var verdict: str = ""
@http(path="/triage")
on msg TriageTicket:
infer:
system: "Classify as: urgent | normal | low. One word only."
user: msg.text
target: verdict # typed state var — parsed + validated
processed = processed + 1
# Route to another deterministic actor — typed send
send DispatchActor <- Route(
ticket_id: msg.ticket_id
priority: verdict
)
@http(path="/stats")
on msg Stats:
response(processed=processed) An actor that calls infer is still an actor.
An actor is deterministic compiled Borz code.
It compiles to four targets: native binary, DEMIX microservice, DENSE nanoservice, or Ephernity smart contract.
It has typed messages, sequential handlers, isolated state, and the full Borz compiler guarantee set.
An actor can call
infer —
an LLM call embedded in a handler step with a typed output variable.
This makes it an agentic actor. Its deterministic core decides
what to infer, what to do with the result, and what to send next.
It is not a pure LLM agent. The control logic is Borz code, not a system prompt.
Handler logic is Borz code. The compiler knows exactly what the actor will do except for the infer result.
Compiles to native binary, DEMIX microservice, DENSE nanoservice, or Ephernity smart contract — without changing source.
infer embeds an LLM call with a typed target variable. The result is parsed and validated before the next line runs.
The actor controls the infer call — its inputs, its target, and what happens after. The actor is the author, not the persona.
Side by side.
The same question answered for each construct.
One typed message bus. Two distinct kinds.
Agents and actors communicate via the same typed message system. An actor sends a typed message to an agent — the agent processes it as an LLM call and sends a typed reply back. The compiler checks both sides of the message contract.
RiskActor runs a deterministic risk model on a transaction.
Score exceeds threshold → sends a typed NarrateAnomaly message
to AnomalyNarrator.
AnomalyNarrator receives the message, runs the LLM call,
and sends a typed AnomalyReport reply
to AuditActor.
msg ScoredTransaction:
tx_id: str
amount_eur: i64
merchant: str
risk_score: f64
actor RiskActor:
@persistent var flagged: i64 = 0
on msg ScoredTransaction:
if msg.risk_score > 0.85:
flagged = flagged + 1
# Send typed message to the agent.
# The compiler checks NarrateAnomaly
# matches what AnomalyNarrator declares.
send AnomalyNarrator <- NarrateAnomaly(
tx_id: msg.tx_id
amount_eur: msg.amount_eur
merchant: msg.merchant
risk_score: msg.risk_score
)
else:
send ClearanceActor <- Approve(tx_id=msg.tx_id) agent AnomalyNarrator:
model: local:ollama:gemma4:12b
memory: rolling(turns: 5)
system: """
Financial anomaly analyst.
Given a transaction and risk score,
write a two-sentence narrative and
recommend: block | hold | approve.
"""
tools:
- MerchantDB.Lookup
- BlacklistDB.Check
# Typed message — compiler-checked
on msg NarrateAnomaly:
tx_id: str
amount_eur: i64
merchant: str
risk_score: f64 msg AnomalyReport:
tx_id: str
narrative: str
recommendation: str # block | hold | approve
actor AuditActor:
@persistent var blocked: i64 = 0
on msg AnomalyReport:
if msg.recommendation == "block":
blocked = blocked + 1
send BlockList <- Add(tx_id=msg.tx_id)
# Typed audit receipt — immutable log
send AuditLog <- Record(
tx_id: msg.tx_id
narrative: msg.narrative
action: msg.recommendation
) Why not merge them?
Keeping them separate preserves what each is good at — and prevents each from inheriting the constraints of the other.
Actors need compile-time guarantees
Targets like DENSE enforce memory caps, flat-ELF binary size, Ed25519 receipts, and deterministic dispatch latency. Ephernity enforces byte-identical execution. These constraints are incompatible with running an LLM. If agent were a subclass of actor, agents would inherit incompatible constraints.
Agents need flexibility actors cannot have
A pure agent may accept arbitrary prompt shapes, maintain a rolling conversation window, and be generalist. Actors must have a fixed, declared message interface. Forcing agents into the actor model would break the open-ended input pattern.
Typed boundary between the two
By keeping them separate with a shared typed message bus, the compiler can check that every agent↔actor handoff is well-typed. The boundary is explicit — not hidden inside an inheritance chain.
Separate routing + deployment
Actors deploy to binary targets (native, DEMIX, DENSE, Ephernity). Agents route to LLM hosts (Claude API, local ollama). The Clan manages both routing paths, but they are fundamentally different infrastructure.
Run agents and actors together.
A Clan runs both kinds side by side. Clan Control routes dispatches to binary actors and to LLM hosts, enforcing policy at every tool call and message boundary.