Okay-effects is a family of algebraic-effect systems for Scala, Java, Go, TypeScript, Rust, Python. A program's type says what it may do. A handler decides how it is done. An interceptor puts each of those actions through a point of control. A sensitive effect, such as a payment, cannot run anywhere but under its audit. The compiler checks this. It is not a convention.
A whole application built on these libraries, live: paste an address and see where the money went, step by step, each step with its reason. It runs in your browser, with no server of ours and no account.
Tools that draw a graph leave the reading to you. Coinpath writes down the rule behind each hop, and the reason it stopped.
An exchange is named only from that exchange's own published list of its wallets, with the date and a link: nothing to take on trust.
A trace is pinned at a block, and a recorded trace replays to the same report with no network at all, so a result can be handed on and checked.
Scala 3 compiled to JavaScript by Scala.js, on okay-scala and okay-ui. The data model is effects: Chain (the transfers of an address), Names (who owns it), Http, Clock, Ui (a screen). Every implementation is a handler: Blockscout turns Chain into Http, the labels' shards answer Names, the browser's fetch answers Http, a recording answers Chain and Names in the tests and the examples. The page is one line that names its handlers. The trace itself never changes between the browser, the tests and a replay; only the handlers do.
An AI agent works through MCP servers: payments, mail, files, code. okay-guard stands between them. You see every call, your policy decides what passes, what is blocked and what waits for you, and you decide on your phone where the call stands. The studio runs in your browser, with no account; when the policy works, run the same guard on your own machine.
Nothing to install to start: add any remote MCP server in the studio and it is called straight from your browser.
Every decision is recorded with the rule behind it. Cases and tests check the policy before it meets a real call.
Export the agent and its policy, import them into the local guard (Node or Java), and the same policy decides there.
Scala 3 and Scala.js, on okay-simple, okay-ui and okay's MCP. The guard is a program over its own effects: the wire, the journal, the policy, a person, the clock. The browser, a Cloudflare Worker that passes messages, Node and the JVM are only their handlers. A call waiting for you is the program's continuation, kept until you answer.
Paste a system — a docker compose file, Kubernetes manifests, or a few lines of its components — and see what each component's loss takes down, how available every entry point is, and which change saves the most downtime. In an incident, give it the logs, the chat and the metrics, and it finds the causes, their probabilities and the check that settles them. Everything is computed in your browser: nothing you paste leaves it.
The architecture, the numbers, the diagnosis and the remedies all ask the same simulator how a failure spreads, so they cannot disagree.
The availability is computed by minimal cut sets and checked by simulating thousands of years; every remedy is checked by spreading the faults left after it.
The same tools as an MCP server: a local model with them named the exact cause of 7 incidents in 8, alone 2 in 8.
Scala 3 on the JVM and Scala.js, on okay-scala, okay-ui, okay-datalog and okay-simple's YAML. The system is a value and its failures spread by one function; the screens are an okay-ui program over it; the tools a model calls are a program over the effects Desk, Files, Http and Clock, whose handlers are the machine's on the command line and in memory in the tests.
Say in ordinary words what you offer or what you need, and the chat finds the other side, asks them for you and passes on the contacts once they agree. Ask it for vacancies by a keyword and a city, and it reads employers' own public job boards. In the browser and in Telegram, in four languages, on Cloudflare's free plan.
Open Okay! Chat In Telegram Source
A message is routed by rules and a lookup table in microseconds. A language model answers only what the rules do not understand, under a daily budget, and may promise nothing.
A deal is a small state machine of its own: nobody's contact is given until the other side says yes, and two answers at once cannot both win.
Workers, Durable Objects, D1, KV and Workers AI, each within its free allowance; the page is static and costs no requests, its messages come by a hibernating WebSocket.
Scala 3 compiled to JavaScript, on okay-serverless and okay-ui. A conversation is a Durable Object, a deal is a Durable Object, the market and the vacancies are in D1, and the cloud's services are effects with Cloudflare's handlers. Telegram, the page and the API are three doors to the same conversations. The page is an okay-ui program: the conversation is its effect Talk, answered over HTTP and the socket. The source, with every step measured: okay-chat-edge.
Smaller examples of the same way of building, to play: each game is one program of effects, its screens as data, its rules pure and tested. Go adds play over the network and a neural network on your GPU.
Guess the hidden five-letter word in six tries. Each guess colours its tiles: green, the letter is in the word in that place; yellow, it is in the word elsewhere; grey, it is not. A small application to read, whole: one program, its effects, its handlers, and a page that is one line.
The program never touches the page. It describes each screen, a picture of the board, a form and its buttons, as a value, and okay-faces draws it.
The hidden word is the effect Secret, the rounds played are Results. In the browser they are a random draw and the page's storage; in the tests, a fixed word and a list.
The model is a toy of 130 numbers that never sees the colours. A mask built into a transformer's weights allows only letters that lead to a word that fits, so it cannot invent a word or break a colour.
Scala 3 compiled to JavaScript by Scala.js, on okay-ui and okay-faces. The model is one more effect, Model, its handler the toy in the page; a language model would be another handler, the program the same. The tests play the whole application as a person would, by the buttons' names, with the word fixed: no mocks, only other handlers. The page is the program with its handlers named, in one line: Page.mount(OnFaces.program(steps(InLocalStorage("wordle-results")(Secret.random()(Wordle.main[App])))), skin).
Fill the grid so that every row, every column and every 3 × 3 box holds the digits 1 to 9, each once. Every puzzle is made in your browser, with exactly one solution, and the hint does not just give a digit: it says why it must be there.
The board is 81 blocks, each marked by what it holds; a free cell carries a button. The program says what is there; the stylesheet draws the grid.
The game asks Puzzles for a puzzle. In the browser the generator answers; in the tests a fixed puzzle does, and the whole game is played by its buttons.
The same rules that make the puzzle find the next forced digit and the units that force it, so every hint can be checked by eye.
Scala 3 compiled to JavaScript by Scala.js, on okay-ui and okay-faces. The page is one line: Page.mount(OnFaces.program(steps(Puzzles.random()(Sudoku.main[App]))), skin).
The ancient board game, on a 9 × 9, 13 × 13 or the full 19 × 19 board. Put stones in turn, surround the other side's groups to capture them, and fence in more of the board. Play a friend at one screen, or play a small model: it can only ever press a legal point, and the page shows when what it wanted was not allowed.
A quick 9 × 9, 13 × 13, or the full 19 × 19 — at one screen, against the model, or online with a friend. Only legal points can be pressed: a taken point, a suicide or a ko simply has no button.
The model is KataGo's network (the g170 run, 10 blocks, 3 million weights, CC0) running in your browser on WebGPU, equal to KataGo itself to six digits. A search reads hundreds of positions with it before each move. No server: the GPU is yours.
The game asks Model for the values of the points. A stronger engine would be another handler: the game would not change.
Scala 3 compiled to JavaScript by Scala.js, on okay-ui and okay-faces. The board is a picture with a grid of buttons over it, one at each legal point; the tests play whole games by the points' names. A game far apart lives on a Cloudflare Worker on okay-serverless: each game a Durable Object, its players and watchers WebSockets, every move checked there by the same rules as in your page; the same server runs on a machine of one's own. The page is one line: Page.mount(OnFaces.program(steps(Model.toy()(Go.main[App]))), skin).
Effects make every action of a program an explicit operation that a handler interprets. That gives one place where each action can be checked. okay uses that place at three points: in the types, at run time, and in the compiled code.
Receipt ! Db + Payments is a program that may reach a database and payments, and nothing else. A program that performs a sensitive operation outside its audit does not compile.
Each operation passes a point that can record it, change it (normalise, cap, mask), or refuse it. An observer can see every operation of every effect. All of these live in the kernel.
okay-audit reads the compiled classes. It confirms that business code reaches no network, files, SQL, clock or threads on its own, and it lists every module that does, by provider: the dependency inventory DORA asks for.
A payment audit in Scala, from okay-core's test suite, shortened:
enum Pay[X]: case Transfer(to: String, amount: Int) extends Pay[String] given Sensitive[Pay] = Sensitive() // from here on, Pay runs only under an audit def guarded[T, A](limit: Int) = new Interceptor[Pay, T, A, A]: def apply[X](op: Pay[X], k: X => A ! T, pass: Pass[Pay, T]) = op match case Pay.Transfer(to, amount) if amount > limit => record(s"refused: $amount to ${masked(to)} over the limit $limit") *> raise("over the limit") case Pay.Transfer(to, amount) => pass(Pay.Transfer(to.trim.toUpperCase, amount)).flatMap: receipt => // changed, then passed on record(s"transfer $amount to ${masked(to)} → $receipt") *> k(receipt) audit(guarded(1000))(payments).handleAudited(bank).handle(trail).run // transfer 100 to ****1234 → ok:ACCT-1234:100 // refused: 5000 to ****T-77 over the limit 1000
The same programs exist in Rust (okay-rust) and TypeScript (okay-ts), so the guarantee does not depend on Scala. Full account: okay-core, docs/intercept.md.
An effect is an interface of operations. A handler implements it. The program is written once, and the caller picks the meaning.
The same code runs against a real database, an in-memory one for tests, or a recording one for replay. Nothing in the program changes, only the handler.
A program lists every effect it uses. There is no stack of monad transformers to order up front and no lift. The order of the handlers is chosen when the program is run.
A handler gets the operation and the rest of the program. It can stop the program like an exception, resume it once with an answer, or resume it many times to search.
The scientific foundation of what is implemented here: Ohad Kammar, Sam Lindley, Nicolas Oury, Handlers in Action, ICFP 2013 (the 18th ACM SIGPLAN International Conference on Functional Programming, Boston). More: what algebraic effects are, and why, with a reading list.
okay-simple is the smallest way in: one typed interface of algebraic effects for Scala 3 (3.3 LTS on) and Scala 2.13, on the JVM, Scala.js and Scala Native. It is released as 0.1.0, and adding it is two lines in sbt.
resolvers += "okay-effects" at "https://okay-effects.github.io/okay-simple/maven" libraryDependencies ++= Seq( "io.github.okay-effects" %% "okay-effects" % "0.1.0", // the interface and the standard effects "io.github.okay-effects" %% "okay-effects-machine" % "0.1.0" // an implementation to run on )
Use %%% for Scala.js and Scala Native. Other modules use the same form: okay-effects-stream, -async, -zio, -cats and the rest. The full list is in the repository index.
import okay.effects.*
import okay.effects.std.*
case class Config(greeting: String, limit: Int)
type Service[X] = (Reader % Config + Writer % String + State % Int)[X]
// the row named once; inside, the words need no types
object service extends std.On[Service]:
def serve(user: String): String ! Service =
for
c <- ask[Config]
n <- get
reply <- if n >= c.limit then tell(s"refused $user: busy").as("try later")
else put(n + 1).andThen(tell(s"served $user")).as(s"${c.greeting} $user")
yield reply
val day = for a <- service.serve("ann"); b <- service.serve("bob"); c <- service.serve("cid") yield List(a, b, c)
// the handlers: a config, a log collected, a counter from 0
run(state(0)(writer[String](reader(Config("hello", 2))(day))))
// (2,(List(served ann, served bob, refused cid: busy),List(hello ann, hello bob, try later)))
run(state(0)(writer[String](reader(Config("hi", 5))(day))))
// (3,(List(served ann, served bob, served cid),List(hi ann, hi bob, hi cid)))
The service reads its configuration (Reader), writes a log (Writer) and counts the requests it served (State). Its type, String ! Service, says exactly that. The row is named once, in std.On[Service], and inside it the words take no types: ask[Config], get, put, tell.
The handlers give the meaning: here a config value, a log collected into a list and a counter starting at 0. In production they would read a file, write to a log system and keep the counter in a store. The program would not change, and the second run, with a different config, shows how easily its behaviour is tested. Nothing runs until run.
The step-by-step page goes on from here: your own effect and its handler, a handler that resumes twice, errors and streams.
The same program on two implementations, in Scala 3 and in Scala 2, and the Scala versions each form needs.
Your own effect, a handler that resumes twice, choice, errors, generators, interception and delimited control.
Interop with ZIO (environment, errors, layers), cats-effect, fs2, kyo and monix. Bring effects into an existing codebase one part at a time.
There is one kernel. A program is data and the rest of the program is a value. The other languages port the kernel, are checked by its test suites, or bridge to it. okay-kernel is a protocol between them: a program in one language runs under handlers written in another, in one process or over a wire. In the demo, a shop runs in Go while its inventory, payments and mail are Scala handlers.
| Project | Language | What it is |
|---|---|---|
| okay-core | Scala 3 | The kernel: one monad, delimited control (shift0), multi-shot continuations as data, no macros, no casts. Interceptors and audit. |
| okay-simple | Scala 3, 2.13 | A small typed interface for both Scala compilers, released as 0.1.0: three implementations, ten modules, and interop with cats, ZIO, fs2, kyo and monix. |
| okay-kernel | Java API | The protocol: a program steps to done, yield, failed or cancelled. It comes with a TCK that every implementation passes. |
| okay-java | Java 25 | The kernel's design on shift0. A capability that escapes its handler is a compile error. |
| okay-rust | Rust | The kernel ported to Rust, plus a separate crate for effects in plain async Rust with capabilities. It is checked against the kernel's suites. |
| okay-go | Go | Direct-style effects on Go's coroutines, at 49 ns an operation. Parallel branches run on goroutines. |
| okay-python | Python | Typed effects on generators, clean under mypy --strict. asyncio needs no coloured functions. |
| okay-ts | TypeScript | Effects tracked in the type as a union. Programs as data resume many times; generators resume once. |
The whole family, with an example in each language: docs/family.md.
okay is over a hundred modules on one model of effects. The core has no dependencies, and one source builds for the JVM, Scala.js and Scala Native.
For AI agents in particular, the agent's every action is an effect, so the controls above apply to it.
Each a Maven repository on GitHub Pages — a resolver, no account. All of them, with how to add each.
Scala 3 and 2.13 behind one interface; Scala 3 alone on a CEK machine. Java (okay-direct) and Kotlin (okay-kotlin) too.
User interfaces on effects: one program is a window, a terminal, a web page, a REST API and an MCP server for an AI agent; on Cloudflare in one line. On okay-http and okay-mcp.
Versioned records and an ORM on effects; policies whose decisions are theorems; a cloud as effects — Cloudflare and AWS, state without replay.
From okay's benchmarks (JMH, average time, lower is better; cats-effect 3.5.7, ZIO 2.1.14, atnos-eff 7.0.4, fs2 3.10.2). Every lane has its protocol written down, and a competitor is measured in the same shape and at the same granularity. A loss is printed as a loss.
| Program | okay | ZIO | cats | atnos-eff | the floor |
|---|---|---|---|---|---|
10 000 flatMaps, built and run | 5.5 | 193 | 163 (IO), 117 (Free) | 286 | — |
10 000 asks, left-nested | 116 | 258 | 346 (Kleisli) | 1 469 | — |
10 000 tells, left-nested | 217 | — | 1 222 (WriterT) | 3 385 | — |
| 213 choices, all collected | 1 645 | — | — | 5 487 | 615 (List) |
| 1 000 bracketed resources, left-nested | 22.5 | 116 | 225 (IO) | — | — |
| Fibonacci by a generator, 1 000 elements | 19.2 | 175 (ZStream) | 268 (fs2) | — | 11.7 (Iterator) |
| Fork/join of 100 fibers | 24 | 46.6 | 121 (IO) | — | 21.8 (raw Loom) |
okay's 5.5 is its opt-in eager encoding, with its hazard stated: construction evaluates. The lazy encodings, which keep the laziness contract, read 95 and 112.
| Pipeline | okay | fs2 | ZIO | the floor |
|---|---|---|---|---|
| map / filter / take(1000) / sum, one chunk | 8.22 | 21.9 | 35.8 | 15.2 (Iterator) |
| the same, okay staged (fused) | 1.70 | — | — | 15.2 |
| two 500-element streams merged by readiness | 13.3 | 94.4 | 51.5 | — |
Wrocław's public transport timetable, event-time windows and keyed state, one JVM per lane. The table shows events per second at 1, 2, 4 and 8 cores.
| Library | 1 core | 2 | 4 | 8 | 1 → 8 |
|---|---|---|---|---|---|
| okay | 4.29 M | 7.62 M | 12.77 M | 22.56 M | 5.3× |
| java.util.stream | 4.30 M | 5.27 M | 6.10 M | 7.16 M | 1.7× |
| zio-streams | 4.14 M | 5.79 M | 6.76 M | 6.96 M | 1.7× |
| fs2 | 4.17 M | 5.11 M | 6.40 M | 6.56 M | 1.6× |
At one core every library is within a few percent of the others, the measurement's own noise. What separates them is the slope. okay fans the job across fibers joined by a merge, and the others hit the fan-in cost of one stream before they run out of cores.
The same job on a standalone cluster of three processes per engine, at 575 k, 1 255 k and 2 414 k events. A least-squares fit separates each engine's fixed cost, paid before it has seen an event, from its marginal rate.
| Engine | 575 k | 1 255 k | 2 414 k events | fixed cost | marginal |
|---|---|---|---|---|---|
| okay, 8 partitions over 4 processes | 101 ms | 141 ms | 203 ms | 70 ms | 18.1 M ev/s |
| Flink 1.20, 1 JobManager + 2 TaskManagers | 742 ms | 736 ms | 1 379 ms | 431 ms | 2.71 M ev/s |
| Spark 4.0, 1 Master + 2 Workers + executors | 3 878 ms | 4 879 ms | 7 014 ms | 2 824 ms | 0.58 M ev/s |
The engines' fixed costs buy what okay's lane does not have: checkpointing, rescaling and recovery, none of which is priced here. Spark's 2.8 s is a SparkContext per submission, which a job running for an hour amortises. Distribution costs okay a constant and not a rate: 19.5 M ev/s in one JVM, 18.1 M across four processes.
| Program | machine, Scala 3 | machine, Scala 2 | Free, Scala 3 | tagless on the machine | okay-core's kernel |
|---|---|---|---|---|---|
| 1 000 maps | 6.9 | 6.8 | 12.0 | 10.1 | 9.1 |
1 000 asks under their handler | 17.0 | 20.8 | 33.8 | 24.6 | 15.9 |
| the same, across four other handlers | 23.5 | 17.0 | 70.1 | 39.1 | 19.1 |
1 000 get/put pairs | 18.3 | 16.1 | 20.4 | 30.1 | 14.0 |
Sources, with every protocol and the raw history: okay's docs/benchmarks.md, the streaming write-up, okay-simple's README.
Definitions, references, callers, implementations, and which given each call site resolves. It reads them from the compiler's own output (SemanticDB, TASTy, SCIP) and serves them over MCP, so an agent asks instead of grepping. It is built on okay's MCP server.
Every build gets a report that an auditor can read. It shows which modules may reach the outside world, and that business code does not. It has zero dependencies and is evidence, not a compliance claim.