(~ solve (x) '(-> ,x "Verify the answer"))
(["Ship one result"] (solve "Find the bug") (solve "Design the fix"))
In two forms this defines a reusable agent graph, fires two instances, routes each real answer through verification, and mediates both verified results into the program's returned value.
Rebis is a pure S-expression language for programming LLM-agent systems.
Quoted strings are raw prompts, bare atoms are Lisp-like symbols, ~ defines
structural macro abstractions, arrows route actual answers, and squares contain
executable mediator code.
program := expr+ EOF
expr := primary
primary := prompt | symbol | numeral | '<>' | '\'' primary | ',' primary
| '(' form ')' | '{' expr+ '}' | '|' expr+ '|'
form := '~' symbol '(' symbol* ')' expr
| '#' module | '[' expr ']' expr+
| '$' expr+
| '?' expr+
| '*' expr expr+
| '@' expr expr+
| '!' expr
| '=' symbol expr expr+
| '&' | '&:' expr
| '+' expr expr+
| '/' model expr+
| '^' expr
| '%' expr expr expr
| '><' expr+
| '->' expr expr+ | '<-' expr expr+
| symbol expr* | expr+
model := host-defined selector, e.g. ollama:qwen4:4b
As in Lisp source files, a program may contain multiple top-level forms without an extra pair of parentheses. They share one lexical definition scope and execute in source order; the parser retains that boundary as an implicit program node.
| … | is the numeric plane: the other pole, where values are quantities and
the operators read as arithmetic — $ adds, [M] folds, ^ inverts, / is a
modulus. Nothing inside fires, so it is free, and quantities are integers and
rationals rather than floats. Text becomes quantity through a crossing such as
([gematria] …) or ([calls] <>), which is how a program measures its own run.
Three forms are about memory and about the program itself. {A B …} is an
imaginary space: everything inside it runs and is traced, and only the space's
own answer becomes evidence — so a program can explore without remembering the
exploration. A ! inside a space keeps its answer for the plane rather than
the run, readable by every later space, which is how a search remembers its own
dead ends without recording them. <> is the program itself, as syntax:
interpolated it contributes its canonical text, in an executable position it
re-enters. (>< A …) fires a prompt and parses the answer as Rebis and runs it, so a
program can write the program it needs — and with <> it can read itself,
write a better self, and run it.
The central form is ([M] A B ...). Branches run first; their actual answers
become RESULT 1, RESULT 2, and so on in source order, then enter mediator
program M. A host may evaluate mutually isolated branches concurrently while
preserving that source-ordered join.
Append a host-defined selector to a self-delimiting expression to route every model call in that subtree through a model other than the host default:
(["Judge both answers"]
"Draft locally"/ollama:qwen4:4b
(-> "Investigate" "Write the report")/claude:opus5)/openrouter:anthropic/claude-opus-4
The outer binding applies to the mediator and every otherwise-unbound branch. The two inner bindings override it only for their own expressions; after either subtree finishes, the outer model is restored. A binding changes routing, not the expression's value or geometry, and does not itself make a model call. Without a binding, execution uses the host's configured default.
The /selector must immediately follow a closing quote or )—there is no
intervening whitespace. This keeps / unambiguous inside ordinary symbols and
module paths such as std/loops. Selectors are opaque to Rebis and may contain
ASCII letters, digits, ., -, _, :, and /; the host decides which
providers and model names they mean.
orchestrate_parallel evaluates every eligible [] child concurrently, bounded
by RuntimeLimits::with_max_concurrency. orchestrate_parallel_with_inlet adds
a thread-shareable input seam. Groups, arrows, gates, and the mediator remain
sequential; branch answers, events, and diagnostics rejoin in source order.
An Oracle may override begin_parallel, try_fire_routed, and
finish_parallel. The routed call includes both the stable
ExecutionScope and the optional model selector. Those lifecycle
callbacks give hosts a stable ExecutionScope for branch-local resources and
put reconciliation immediately
before the mediator. Returning Ok(false) from begin_parallel safely falls
that square back to ordinary sequential execution. Rebis itself remains
zero-dependency and filesystem-free; a host can supply containers, copies, Git
worktrees, or no external isolation at all.
Lazy control is explicit: (% condition when-one when-zero) evaluates the
condition first, requires exactly 1 or 0, and evaluates only the chosen
continuation. A square is never conditional; [] always mediates all of its
branches. The embedded std-binary protocol uses $ to ask for one exact
decision token before the gate validates it.
([(->
"Compare all reports"
"Resolve disagreements"
"Write one verified fix")]
"Inspect the code and reproduce the failure"
"Trace the execution and find the root cause")
Macro abstractions use (~ name (parameter ...) body) and ordinary Lisp-style
calls:
(
(~ inspect (target)
(-> target "Write a detailed report"))
(inspect "Inspect the parser"))
Application substitutes argument expressions structurally. It never changes text inside quoted prompts. Only quoted prompts fire agents.
The string is the language's fundamental value. ($ A B ...) is the one
operator that transforms it: it interpolates its operands into one string
and yields that string — pure text construction, nothing inside $ fires or
runs. An operand contributes its text: a prompt its characters, a symbol its
bound value, a macro its expanded text (not fired), a nested $ its assembled
text. The assembled string is a prompt in the position the $ sits, so it fires
there, once. Because it is an operator, not in-string interpolation, it never
peeks inside a quoted string — "it cost $100" stays literal.
Variables are macro parameters. A macro binds names; a call supplies the
values, which $ weaves in as text — reused freely, with no extra model call
per use:
(~ case (self rival)
($ "Make the strongest case that " self " beats " rival " in hip-hop."))
(["Deliver the verdict: who has the greater hip-hop legacy, and why?"]
(case "Jay-Z" "Kanye West")
(case "Kanye West" "Jay-Z"))
This fires exactly two advocates and one mediator: self and rival are
text, so reusing them does not multiply model calls.
A text constant is just a macro whose body is a prompt — $ interpolates
its text without firing it, so no quoting is needed:
(~ topic () "the fall of Rome")
($ "Write a short explainer on " (topic) ".")
; one model call — (topic) is woven in as text, not fired on its own.
To carry a model-computed value into a prompt, use -> (it flows the answer
in as INPUT:); $ builds text, -> carries results.
(& port body) receives an external input under the name port, then runs
body with that name bound to whatever the host supplies:
(& input (-> input "Summarize this"))
; `input` yields the host value and flows into the prompt as its INPUT:
The host decides what fills the port — a prior run's answer, another running
agent's output, a line typed at a terminal. Using the port name where a value
flows (an arrow producer, a $ operand) yields that value; no model fires for
the port itself. The core stays pure: the value is fixed for the run. A host may
block inside its input seam until the value arrives, so & is the
language's "stop until it receives input" — the building block for one agent
feeding another and for long-lived, supervised agents.
(^ E) takes the orientation dual of Rebis syntax without calling a model. It
recursively exchanges -> and <-, preserves written operand order, and
leaves prompts, symbols, imports, and the complete contents of $ fixed:
(^ (-> "gather evidence" (<- "write report" "challenge evidence")))
; becomes (<- "gather evidence" (-> "write report" "challenge evidence"))
It is an involution: (^ (^ E)) is exactly E. Groups, squares, and quotes
retain their shape while their children are dualized. Macro definitions stay
fixed; a call expands first and its resulting graph is inverted, so a local
definition and its call are dualized exactly once.
Macros are higher-order: a named macro symbol can be passed as an argument and substituted in call-head position.
(
(~ apply (worker target) (worker target))
(~ inspect (target) (-> target "Write a report"))
(apply inspect "Inspect the parser"))
Repeated parameters may duplicate model work, so hosts should enforce expanded call, size, token, and time budgets.
For explicit Scheme/Common-Lisp-style construction, quote holds output syntax and comma splices caller syntax:
(~ twice (work) '(-> ,work ,work))
A quote is also a value, so a program can hold a program:
(= plan '(-> "Investigate this" "Write it up")
((plan) ($ "That was produced by: " plan)))
Holding one fires nothing, and neither does interpolating it — so a run can
weigh what it is about to spend before spending it. Calling the name runs it,
which is eval without an eval operator, and '() is the empty list.
What no program can do is turn text into structure: a model's answer, bound and
called, is an undefined macro rather than an execution. Syntax has exactly two
written constructors, ' and <>, plus ><, which fires — so the prompts a
run can produce stay bounded by what was written plus what was paid for, and a
program's price is still countable by reading it.
Macros may call themselves. The % binary gate evaluates its condition as an
exact 1/0 decision and runs only the selected branch. This provides loops
without a separate recursion operator;
the reference runtime caps expansion at 256 macro calls.
(# module) imports the top-level macro definitions of a host-resolved Rebis
module. Kaos resolves modules from saved hypersigils in ~/.kaos/sigils; names
may be qualified, such as std/loops. The crate embeds twenty-six modules under
that reserved namespace: import one leaf with (# std/loops), or the complete
folder with (# std). Hosts may apply the same definition-only expansion to
their own folders; Kaos recursively imports a saved folder such as (# team).
Module bodies are definition-only and may re-export other modules with #.
For example, save this as the hypersigil engineering.rebis:
(
(~ investigate (issue)
'(->
,issue
"Reproduce the failure"
(["Choose the strongest causal explanation"]
(<- "Challenge the trace" "Trace state backward from the symptom")
(-> "Inspect the relevant code" "Propose the earliest wrong state"))))
(~ repair (issue)
'(->
(investigate ,issue)
"Implement the smallest root-cause fix"
(<- "Return the reviewed patch" "Run tests and search for regressions"))))
Then a program can import and expand it twice:
(
(# engineering)
([(->
"Compare both repairs"
"Resolve contradictory evidence"
"Write the final design and patch plan")]
(repair "Tower middleware lacks ad-hoc span metadata")
(repair "Future cancellation can lose the closing event")))
The compact call:
(repair "Future cancellation can lose the closing event")
structurally expands into the full nested ->, <-, mediator, and
investigate graph from the module. Arguments remain syntax throughout the
expansion; no prompt text is interpolated or reparsed.
kaos rebis run --allow-tools examples/incident.rebis
kaos rebis run --dry '(["Combine reports"] "Inspect code" "Trace failure")'
kaos rebis tree '(["synthesize"] "Inspect code" "Trace failure")'Kaos provides a direct editor plus an optional Vim-like mode with
normal/insert/visual modes and % bracket matching. ctrl / opens Kaos commands
(/run, /tree, /mandala, /format,
/panel, /graph); : remains reserved for Vim file commands. See the specification,
guide, symbol reference,
standard library, and host notes.