Tech Lead · Software Architect · Agentic AI EngineerOpen to new work · İzmir, Turkey · UTC+3
Note01
The AgentEff architecture
AgentEff is a software factory of Effect 4 actors, driven from Slack, in which the models propose and deterministic code decides. It is in design: planning finished in October 2026, and the build is starting.
AgentEff is a software factory Emre is designing. People direct it from Slack, and it is meant to take work from a request to tested software running in environments it manages itself.
This post describes a design, not a system. Planning finished in October 2026 and the build is starting; nothing on this page has run in production, and there are no results to report. Where a number appears, it is either a design target, and says so, or sample data in the illustrative scenario at the end.
The design hangs from one principle: deterministic workflows, probabilistic intelligence. The agents in AgentEff — one Director, Squad Leads under it, Workers under them — are model-driven, and a model may propose a plan, a diff or a risk score. Whether that proposal is acted on, charged, retried, approved or deployed is decided by deterministic code that can be read, tested and replayed. No guarantee in the design depends on a model behaving.
The drawings below come from the architecture spine, which records forty-two numbered decisions, AD-1 to AD-42, each with what it binds and what it prevents. The AD numbers beside each drawing say which decisions it shows.
The paradigm is three ideas used together: event-sourced actors, a functional core and ports and adapters. Every stateful unit is an Entity on Effect Cluster whose state is a fold of its own event stream. Workflows orchestrate, and hold no business fact that is not also on a stream. Business decisions are pure functions. Every I/O slot is a port, a Context.Service, filled by an adapter, a Layer.
Layers are only worth drawing if the arrows between them are enforced. Each unit is its own Bun workspace package, and a CI import-graph check over package dependencies rejects any edge that is not in the drawing: domain imports nothing, core never imports an adapter, and no adapter imports another. Every port ships a contract test suite that each of its adapters runs, which is what makes a slot swappable rather than nominally pluggable. It is the functional core on Systems, with the boundary checked by the build instead of by review.
Scroll sideways to see the whole drawing.
Arrows are the only allowed imports. A CI import-graph check over the workspace packages rejects any other edge (AD-2), and every port’s contract suite runs against each of its adapters.
Each port and its v1 adapters
Slot
Port
v1 adapters
Event Store
EventStore
PostgreSQL
Model providers
LanguageModel (gateway only)
OpenRouter; an own Bedrock Layer; OpenCode Go, optional
Decision Model
DecisionModel
TypeSafe, Cloudflare
Harness · Sandbox
Harness · Sandbox
pi with harness-ext, Claude Code · Incus
Compute · deploy targets
Provisioner · DeployTarget
Alchemy v2 on Hetzner, Incus · Tenant Environments
Integrations
Tracker · Docs · Forge · Chat
Linear · Notion · GitHub and Gitea · Slack
Secrets · notification · telemetry
SecretLeases · Notifier · Telemetry
self-hosted Infisical · Slack, Pushover · OpenTelemetry to LGTM
Memory and Knowledge
MemoryStore + KnowledgeStore
PostgreSQL, behind one facade
Part02
One way in, one way out
AD-4
AD-5
AD-17
AD-31
AD-35
AD-37
AD-42
Humans speak through Slack and tools through webhooks, and everything enters at apps/ingress. Ingress verifies the envelope, Schema-decodes the delivery into an …Observed event, derives a commandId from the delivery ID and routes it to exactly one owning Entity. Everything leaves through the outbox: an intent event, one adapter call, then a result event back to the owner.
A Slack message arrives as one of three types, and only two of them can change anything. A Command needs an Operator or an Engineer acting within their role. A Client Owner’s signed answer to a question routed to them is an Approval, never a Command. Everything else — including an instruction found in a repository file or in a model’s output — is Content, and causes no action.
The Sandboxes are where the models work, so they see the least. A Sandbox can reach the Model Gateway and its egress proxy and nothing else; it holds a short-lived gateway lease and no Slack, Linear or Notion credential at all, and forge tokens are injected by the proxy without the Sandbox ever seeing them. Its one write of its own is a git push to its Run’s branches. Every model call in the Factory, from any Agent, goes through the one gateway, which is also the meter.
The alarm for a dead Factory lives outside it. RootSupervisor sends a heartbeat to a dead-man’s switch that shares no infrastructure with the Factory, and the switch alerts the Operator when the heartbeat stops.
Scroll sideways to see the whole drawing.
One way in, one way out. Ingress decodes each delivery into an Observed event (AD-4); the outbox makes every external write (AD-5); the gateway is the only model path and the meter (AD-31); the egress proxy injects forge tokens a Sandbox never sees (AD-17).
Part03
A hierarchy, not a swarm
AD-11
AD-21
AD-26
AD-30
AD-33
The Director is the root Agent and the Operator’s counterpart. It delegates to Squad Leads — PM, Dev, QA, DevOps and SRE — and they delegate to Workers. Each Agent is exactly one Entity with its own mailbox and stream, and a Command reaches an Agent only from its parent. The Director and the Squad Leads never implement; only Workers run Sandboxes and write to repositories.
Above the Agents sits RootSupervisor, which has no model port at all. It judges sickness only from signals no Agent controls — the gateway journal, Run progress, runner heartbeats — and treats it from a fixed list: reply, retry, replay, interrupt, re-instantiate, die. A restart rebuilds the Agent from the Event Store; past its restart limit, an Agent stops restarting and the Operator is told.
A freeze is enforced where the frozen Agent cannot reach. The Audit Watcher is a deterministic rule set with no model in it, and a rule hit or an Operator Command can freeze an Agent, a subtree or a whole Tenant. RootSupervisor publishes the freeze as a versioned view; four enforcement points outside every Agent — the gateway, the egress proxy, the outbox and the admission check — evaluate the same pure isFrozen on every action, and refuse when their view is stale. Only an Operator can lift it.
Scroll sideways to see the whole drawing.
Freeze is enforced where the Agent cannot reach. The gateway, the egress proxy, the outbox and the admission check each evaluate the same pure isFrozen against a versioned FreezeView, and fail closed when it is stale (AD-33).
Part04
The write path
AD-3
AD-4
AD-5
AD-7
AD-8
Every state change has one shape. A command, carrying a commandId derived as a uuidv5 of its cause, reaches the single Entity that owns the stream. A pure decide(state, command) returns events — no I/O, no clock, no model — and they are appended at the expected version. A command whose ID is already in the stream returns the earlier result and appends nothing, so a redelivered webhook or a restarted reactor is harmless.
Readers see an event only once every lower position has committed, and projectors and reactors consume by position. Side effects run only as outbox intents with result events — the outbox on Systems, taken all the way. A crash between a call and its result yields …InDoubt, and an irreversible step in doubt is never retried automatically; it goes to the role that owns it. Every Tenant-scoped table carries a row-level security policy on tenant_id, and CI rejects a migration that adds one without it — tenancy enforced by the database, not by the caller.
Scroll sideways to see the whole drawing.
Idempotent at every hop. A commandId is a uuidv5 of its cause, so a repeated delivery appends nothing (AD-3); readers follow commit order (AD-7); every external write is an intent with a result (AD-5).
Part05
A Run, end to end
AD-6
AD-16
AD-31
AD-40
AD-41
A Run is one execution of a Procedure, and one cycle of a Ticket is one Run. Its facts — attempts, the model tier, the pinned version of everything it uses, its status — live on a Run Entity’s stream, not in the Workflow’s journal; the Workflow only orchestrates. Each attempt begins with StartAttempt, which the Run Entity refuses once the pinned retry budget is spent, and no command a Worker can reach resets it.
Model work never runs in an Entity’s handler, which only decides, appends and starts Workflows. It runs as a Workflow Activity: the Worker drives the pi harness over RPC inside its Sandbox, and each model call goes to the gateway, which resolves the Agent, Run, Budget node and tier from the lease, asks the leaf Budget Entity to reserve the spend, forwards only on a grant, then settles and journals the pair.
Because every pair is journaled, a replay needs no model calls: the Workflow re-reads its Activity results, a replay provider serves the journal, and the outbox swaps its adapters for recorders that do nothing. A refusal from the gateway is a closed, typed set, and each kind maps to one fixed outcome for the attempt.
Scroll sideways to see the whole drawing.
The facts live on streams, not in the journal. Only the Ticket’s Entity starts the (ticketId, cycle) Workflow, and only a Human raises the cycle (AD-6). The Sandbox holds a gateway lease and no provider key (AD-16, AD-31).
BudgetExhausted · Frozensuspend the attempt; no retry budget spent
BreakerTripped · ProviderRateLimitedwait on a durable timer
TierRefused · ProviderNotAllowedfail the attempt, non-retryable
Part06
The decision ladder
AD-13
AD-20
AD-35
AD-36
Who must approve an action is decided by a pure function, a ladder of six rungs. Its inputs are the step’s declaration, the Ticket’s risk, the paths in the diff and a Decision Model’s risk score compared with the Mode’s threshold — so a model’s output is one input among several, never the verdict. Rung 1 rejects on first match. Rungs 2 to 5 each add an approver, and all of them must approve. If no rung matches, the action is reversible and runs on its own, and the decision is logged with its rungs, inputs and confidence.
An Approval binds to the exact bytes it approves. Every request carries a subjectDigest, the hash of the inputs the step will act on, and the step recomputes it when it runs: any change voids the grant. A grant that covers several items is refused for an irreversible step or a Production deploy.
Production has a single door. A Production release changes only through SetProductionRelease, which is accepted only with a Human Approval whose digest is that of the Production-Readiness Certificate, and the certificate is built and signed by a model-free step outside every Sandbox. During an incident, a declared containment step may switch traffic back to the last approved release, or turn off a kill-switch flag, without waiting for anyone — but it never changes the release of record, and an overlay nobody confirms escalates instead of quietly flipping back.
Scroll sideways to see the whole drawing.
Rejection is first-match; Approvals accumulate. The same inputs always give the same rejection or the same set of approvers. The yellow rows are Humans (AD-13).
Scroll sideways to see the whole drawing.
A grant is bound to its subject. The step re-checks the digest when it executes, and the Deployment Procedure’s last step only issues SetProductionRelease; it never deploys itself (AD-13, AD-20, AD-36).
Scroll sideways to see the whole drawing.
Containment without a deploy. An overlay is laid over the release of record and never changes it; the two durations are Config values, not fixed numbers (AD-20).
Part07
Memory and Knowledge
AD-14
AD-15
AD-32
The Event Store is the only truth, and what the Factory learns is split in two. Memory is what an Agent writes freely about its own work, anchored to a Ticket, a Project or a Squad, and it needs no Approval. Knowledge — Domain Rules, decision records, stack and Factory conventions — changes only by one of three paths: an Approval, a Project-level decision record ratified by the owning Squad Lead and revocable by a Human, or a setting edit observed from the tool. Domain Rules and stack- or Factory-wide Knowledge always need a Human’s Approval. Promotion is the only path from Memory to Knowledge: the consolidation pass the design calls Dreaming may write Memory and may only propose Knowledge.
Every write is scanned for secrets before it is decided, and holds a pointer to a variable’s source, never its value. Staleness is decided by a deterministic projector — an item turns suspect when its anchor changes and expires on a timer — never by a model, and Agents read through one facade that serves a stale item labelled rather than trusted. Retrieval in v1 follows typed links in Postgres, with no knowledge-graph engine.
Personal and Tenant-confidential fields are encrypted with per-Tenant or per-subject keys at every serialization boundary from the first phase, and the keys live outside the Factory’s database and its backups. That is crypto-shredding readiness: erasing a subject means destroying a key, in an append-only log that otherwise never forgets.
Scroll sideways to see the whole drawing.
Promotion is the only path from Memory to Knowledge. Dreaming writes Memory and may only propose; staleness is decided by a projector, never a model (AD-14, AD-15).
What it costs A lost key is lost data by design. The fields it encrypted cannot be read again, and the design accepts that as the price of an erasure that works in an append-only log.
Part08
The shape on Hetzner
AD-18
AD-23
AD-24
AD-25
AD-42
The Factory has three environments: local, which is a Nix shell and one runner; staging; and production. Staging is isolated — its own database, Slack app, Linear team, keys and Budgets — and never receives production webhooks or credentials, and a change to the Factory itself reaches production only with the Operator’s Approval. Hosts are reproducible NixOS machines, installed from pinned images, and provisioning is Alchemy v2 stacks run through the Provisioner port.
Production is four kinds of Machine Pool on one private network whose default is deny. control runs PostgreSQL, ingress, the gateway and the runner for the Director, RootSupervisor and the Squad Leads. shared-workers runs Worker runners and their Incus Sandboxes, each behind the host’s network ACL and an egress proxy. secrets runs a self-hosted Infisical with its own database, on a failure domain apart from control, and hosts no Agent. Each Tenant gets a tenant:<id> pool that hosts its own client app’s Staging and Production and no other Tenant’s.
A backup counts only after a restore drill has restored it. The design targets a recovery point of 15 minutes or less and a recovery time of 4 hours or less, with a monthly drill that also restores the secrets plane and proves the encrypted fields still decrypt; the sizing target is 20 concurrent Sandboxes and their runners. Those are design targets. None of them has been measured, because nothing has been deployed.
Scroll sideways to see the whole drawing.
Separate failure domains by design. The secrets pool shares no database or host with control (AD-42), and a Tenant pool hosts only that Tenant (AD-23). Every value in this drawing is a design target.
What it costs v1 runs one secrets server without high availability, and a Sandbox snapshot lives only on its host, so losing a host loses its snapshots. The Event Store, the Run’s branches and replay survive; the design names both as v2 options rather than v1 promises.
Part09
All of it at once
AD-3
AD-5
AD-6
AD-11
AD-12
AD-26
AD-31
AD-33
The drawings above are still pictures of one mechanism each. The one below runs them together, as a scripted scenario: one Factory and three example Projects, one per stage — AgentEff building itself, a client project releasing, and a new project being planned. Messages move between mailboxes, every state change lands in the Event Store, model calls draw down each Project’s Budget, RootSupervisor restarts a crashed Worker, and the Audit Watcher freezes a subtree until the Operator lifts it.
Watch the obligations strip in each lane. A promise such as a ReplyOwed lives on the owing Agent’s stream, is opened in the same append as the message that caused it — before any model call — and closes only on a typed event, ReplyPosted or the Run’s RunFinished, never on an Agent’s say-so. The design target is an acknowledgement within 10 s, even from a sleeping Agent.
It is an illustration, not a recording. The Projects are examples, and the tenants, Agents, event names, positions, budget levels and restart limit are sample data, with the timing compressed. Select any actor to see what it owns, or a concept card to jump to the moment it happens. If the reader’s system asks for reduced motion, the scenario starts paused, and Step moves it one message at a time.
Illustrative scenario · example projects · sample data
00:00 / 01:50step 0 / 71
NOWThree Projects, one Factorysample dataAgentEff builds itself, a client project (example) is releasing, and a new project (example) is still being planned. Press Play, or Step through one message at a time.
Scroll sideways to see the whole drawing.
EVENT LOG newest first · scenario time
00:00Client projectw-12 is running the client project’s acceptance suite in its Sandbox, as an Activity of run/CP-88.AD-6
00:00FactoryThe Director, pm-lead and the DevOps Squad are asleep. Sleeping Agents make no model calls.AD-11
Born, Grow and Sleep are Lifecycle states (AD-11). Awake marks a Sleep → Grow on the next message, Working means an Activity is running (AD-6), and Crashed, Restarting and Frozen are what supervision and freeze do to an actor (AD-26, AD-33). A sleeping actor is dimmed.
MESSAGEStell, the sender does not waitask, the sender waitsreplyevent appended to the Event StoreAgent tree, parent to child
COLOUREntity messagemodel callHumantoolsupervision, alarm or refusalfreezeread from the Event Store
Every move is a message, and every change is an event. No Agent writes another’s stream or calls a tool or model directly: the outbox, the Model Gateway and RootSupervisor stand between each Agent and the world (AD-3, AD-5, AD-31, AD-33). The three Projects are examples; tenants, Agents, event names, positions, budget levels and the restart limit are sample data, and the timing is compressed.
End—
What this does not show yet
None of this is running. The spine is final, and the build starts with the foundations: standing the Factory up and commanding it from Slack, then having it deploy itself to Hetzner. Everything after that is a plan, and is described here as one.
Writing on this site is meant to cover the mechanisms, what they cost, and where they turned out not to hold. This first post can only do the first two. The third needs code under the decisions, and a later post will say which of them held once there was.