Appearance
Rein language reference
Status: proof of concept. Private. Rules here drive the checker and the evaluator. Design reasons and open questions live in decisions.md.
Rein mimics TypeScript syntax and types, except @ references. A reader who knows TypeScript can read a Rein file. Rein is not TypeScript: it has no imports, no loops, no global scope, and no default or else. A function gets its values only from its parameter and its answers.
Topics
Each topic file keeps its section numbers. Read the file for the rule you need.
- Project layout: section 1, flows and scopes (1a).
- Lexical rules: section 2.
- Grammar: section 3, and the parser errors.
- Functions and parameters: section 4, functions, doc comments and parameters.
- Structs: section 4, inline and shared structs.
- References: section 4, relative and program references.
- Std and templates: section 4, std names and templates.
- Inputs: section 4,
stringparameters. - Async requests: section 4,
requestanswers and struct answers. - Calls with return: section 4,
awaitof a function. - Statements, conditions and values: sections 5, 6 and 7.
- Checker rules: section 8, errors and warnings.
- Evaluation: section 9.
- CLI commands: section 10, commands and output formats.
- Editor tools: section 10, language server and debug adapter.
- Stepping: section 11, the agent interface.
- Agent integration: section 12, agent hooks.
1. Project layout
- A flow root is a directory that contains a
main.reinfile. A flow is a flow root found through scopes (section 1a), or a directory passed to a command. A program can also be one.reinfile: its folder is the flow root and its name is the entry (section 1a). - Each function is one file with the
.reinextension. - A shared struct is one file named
<name>.struct.rein(section 4, Structs). It is not a function.rein graph, the reachability warning, and workspace symbols as functions leave it out.rein showprints it. - Every flow root has a
main.rein. It is the agent's entry point. The checker reports a flow root without it. A program run from its own entry file needs none (section 1a). - A function name is the file path relative to the flow root, without
.rein, using/as separator.ci/fix.reindefinesci/fix. The rootmain.reindefinesmain. - The name of the
export default functioninside a file is a TypeScript identifier. It need not match the file name. - Subdirectories are allowed.
- A function that exactly one other function calls lives in a folder named after that caller, next to the caller's file. The only caller
<dir>/<name>.reinputs the callee at<dir>/<name>/<callee>.rein.merge.reinis the only caller offix_ci.rein, so the file ismerge/fix_ci.rein. Then the folder structure shows the program. - The entry function (
main, orentryinrein.json) is the exception. The program root already is its folder, so a function that only the entry calls stays next to it at the root. - A function with two or more callers is shared. It stays where it is. No rule puts it in a place yet. A later rule can put it at the nearest common folder of its callers.
- The rule does not cover std items,
.struct.reinfiles (a struct is not a function), functions with no caller or not reachable from the entry (the reachability warning covers them), or references to other programs. - A function whose target path another file already has gets no warning, because it cannot move there. When two functions have the same target, such as
x/leafandy/leafwith the one callera, each warning names the other and has no quick fix. Rename one of them first. - A function whose only caller sits in the function's own folder, such as
xcalled only byx/y, gets no warning. The caller would move with it, so the function could never reach the caller's folder. The same holds when the function's folder holds the entry, such asappwith the entryapp/start: the entry would move andrein.jsonwould name a file that is gone. A rename or move that would carry the entry along is refused. - The root
mainnever gets the warning: the project needs it, and amain.reinbelow the root makes its folder another flow. A target path that runs through a folder the loader skips (a hidden folder,target,node_modules, or a folder with its ownmain.rein) gives no warning, and a rename or move into such a path, or one that puts amainbelow the root, is refused. The function would drop out of the program. - A function in the wrong place is a warning that names the target path. The language server has a quick fix,
Move to <path>, that moves the file and rewrites every reference to it and every relative reference inside it. The folder of functions nested under it (<name>/) moves with it, so they stay in place. A rename of a function also renames that folder. A callee keeps its file name when it moves. A rename, move or quick fix is refused, with the reason, when the function's folder (<name>/) or a folder inside it holds a file that is not a.reinfile of the project (such asnotes.md) or a folder the loader skips or a folder with its ownmain.rein; or when the folder or anything inside it is a symbolic link, whether it points to a file or to a folder. The symlink check covers the function's folder and everything inside it, files and folders alike. It does not cover the ancestor folders of the function, or the destination path. Hidden files and editor and OS leftovers (.DS_Store,Thumbs.db,desktop.ini,*.swp,*.swo,*~) do not block a move. They are renamed with the folder, so they land in the new folder. A relative reference that does not resolve keeps its target, in a moved file or in a file that stays put. The move is refused only when that reference would then resolve to a function or shared struct, such as@../cina/bwhen a rename ofatoccreatesc.rein, or@./cinxat the root, which stays put. A reference that leaves the project root, such as@../../nope, is treated the same way: it blocks the move only when, written from the moved file's new folder, it would name a file. On a case-insensitive file system a case-only rename, such asatoA, is allowed: the new path is the file itself, not an existing file. A case-only rename of a function that has a folder of nested functions (<name>/) is refused with the reason, since a case-only rename cannot move the folder: rename it in two steps through another name. A destination counts as the source itself only when its path equals the source path ignoring ASCII case and it is the same file, checked without following links; a hard link or a symbolic link to the source is an existing file, so the move is refused. A rename or quick fix never sends an edit that writes the text already there. The nesting check reads the saved files on disk, so an unsavedmain.reinin a subfolder does not count until it is saved. - A move changes the function's path, so it changes its answer keys (section 11) and breaks a
@program/<path>reference to it from another program. The warning says so. - A relative reference (
@./,@../) resolves only inside the same flow root. A program reference (@program/function) hands off to another installed program (section 4, References). - The directory walk skips hidden directories (names starting with
.),target/andnode_modules/. - A subdirectory with its own
main.reinis another flow. The walk does not enter it. - The walk follows symlinks. A symlink loop does not repeat a directory.
- A
.reinfile that cannot be read (for example, not UTF-8) is an error on that file. The file stays in the project, with empty text. The message iscannot read file: <io error>. - When two sources map to the same function name, the later one wins. This applies to editor overlays only.
1a. Flows and scopes
Programs (flows) live in scopes: project, user, and system. The folders, the manifest, precedence, and shadowing are in Install a program.
rein check <dir>,rein eval --root <dir>andrein run --root <dir>still work on any flow root passed explicitly.- A program is also one
.reinfile, the file form.rein step <file.rein>,rein run <file.rein>andrein check <file.rein>take it: the file's folder is the flow root, and the file name without.reinis the entry. No folder, manifest or scope is needed. Scopes andrein listonly find programs by name. - Imports do not exist. Relative references (
@./,@../) stay inside the flow root. A program reference names another installed program.
2. Lexical rules
A line comment starts with
//and ends at the end of the line.A doc comment starts with
/**and ends at the first*/. Its raw text is the doc block (section 4)./**/is an empty doc comment.A plain block comment
/* ... */is not supported. The error isuse//for comments or/** */for the doc block.An unterminated doc comment is an error:
unterminated doc comment; add */.A STRING is text in double quotes or single quotes. Escapes:
\",\',\\,\n. A raw newline is not allowed inside a STRING.A TEMPLATE is text in backticks. It may span lines. Dedent: the least indent shared by its non-blank lines after the first is removed from each of those lines. Extra indent past it stays. This departs from TypeScript, which keeps the indent. Style: write each continued line at the indent of the line where the template starts. Example: a
returnat 2 spaces, with the template's second line also at 2 spaces, has the textTell the customer a person+ newline +will reply., with no spaces after the newline. An escaped\nis not a line break and does not trigger the dedent. It can interpolate a name with${name}. Only a bare name is allowed inside${}. Escapes:\`,\\,\$,\n.String kinds. Text uses backticks; a tag uses quotes. Text needs line breaks and
${name}; a tag reads as a TypeScript literal type.Position Form Example Return value (an instruction or a final answer) TEMPLATE return `Merge the pull request.`;Call argument value: std templates ( delegate,askUser), project templates, hand-offs and calls with return (await @./x(...)) with argumentsTEMPLATE, or a bare name of a stringparameterdelegate(model=`use opus model`, task=`review ${pr}`)request(...)argumentTEMPLATE, or a template call await request(`Run the tests.`)Union member in a parameter, field or answer type STRING ci: "green" | "red"===and!==operandSTRING ci === "green"caselabelSTRING case "green":A tag may use double or single quotes. Why: both are TypeScript string literal types, and one quote style is a formatter's job, not the compiler's.
rein.jsonis JSON, not Rein, so the rule does not apply to it.A line holds at most 80 characters. This is the Prettier default
printWidth(https://prettier.io/docs/options#print-width). The rule counts characters, not bytes, and a tab counts as one character. It applies to every line of every.reinfile,.struct.reinfiles, doc comments and strings too. A program sets another limit withmaxLineLengthin itsrein.json: a positive integer, such as"maxLineLength": 120.A blank line sets off each
iffrom the code around it. The line above anifis empty, unless theifis the first statement of its block (theiffollows the{that opens the block). The line after anifblock's closing}is empty, unless the next non-empty line is a}, or the file ends. A//comment line directly above anifcounts as the line before it, so the empty line goes above the comment. Rein has noelse(section 5), so there is noelse ifchain. The rule is a warning, not an error:add an empty line beforeif`` oradd an empty line after theifblock. A program with these warnings still runs. Why: see decisions.md.A REF is
@followed by./or../, then path names made of letters, digits and_, separated by/. It has no extension. Examples:@./ci/fix,@../escalate. A REF that does not start with./or../names another program:@programor@program/function. The program name is asrein listshows it, and may hold-. The function path holds names of letters, digits and_. Examples:@pr-handling,@pr-handling/main. A malformed one is an error:a reference is@./path,@../path, or@program/functionfor another program.An IDENT matches
[A-Za-z_][A-Za-z0-9_]*. A declared name (a parameter, a struct field, an answer) is snake_case, as paths are:is_draft,next_step. A name with an uppercase letter is a warning that gives the snake_case name.Keywords:
import,from,export,default,function,if,else,switch,case,return,boolean. A keyword cannot be a name.Reserved words:
for,while,do,const,let,var. They are not Rein. Using one gives a helpful error (section 3). A file name segment cannot be one either.constis allowed only for an answer (section 4, Async requests).string,asyncandinterfaceare names with a role in a type, a function header, or a struct.awaitandrequestare names with a role in an answer.Operators:
===,==,!==,!=,&&,||,!,|,(,),{,},,,:,;,=.==is the same as===, and!=the same as!==. The messages and the graph show===and!==.
Any other character, such as
#, is an error:unexpected character.A UTF-8 byte order mark at the start of a file is whitespace.
3. Grammar
file := item* // one function, or one struct in a `.struct.rein` file
item := DOC? ("export" "default"?)? "interface" IDENT "{" field* "}" // a struct (section 4, Structs)
| DOC? "export" "default" "async"? "function" IDENT "(" params? ")" block
params := param ("," param)* ","? // more than one param is a checker error
param := DOC IDENT ":" type // a scalar parameter. DOC is required; the checker reports a missing one
| "{" IDENT ("," IDENT)* ","? "}" ":" (IDENT | REF) // a struct parameter, destructured
field := DOC IDENT ":" type (";" | ",")? // DOC is required; the checker reports a missing one
type := "boolean" | "string" | "|"? STRING ("|" STRING)*
block := "{" stmt* "}"
stmt := if | switch | return | answer
if := "if" "(" expr ")" block // no else: see section 5
switch := "switch" "(" IDENT ")" "{" clause* "}"
clause := "case" STRING ":" (block | stmt*) // a label with no body joins the next body
answer := DOC "const" IDENT ":" (type | IDENT | REF) "=" "await" awaited ";"? // IDENT or REF: a struct answer (section 4, Struct answers)
awaited := "request" "(" value ")" // ask the agent to do a step (section 4, Async requests)
| REF | REF "(" args ")" // a call with return (section 4, Calls with return)
return := "return" (STRING | TEMPLATE | REF | REF "(" args ")" | IDENT "(" args ")") ";"? // REF with args: a template call or a hand-off
args := arg ("," arg)* ","? // the args of a call or a hand-off
arg := IDENT "=" value // named, as in Python. A value with no name is a checker error
value := STRING | TEMPLATE | IDENT | IDENT "(" args ")" | REF "(" args ")"
expr := and ("||" and)*
and := unary ("&&" unary)*
unary := "!" unary | atom
atom := "(" expr ")" | IDENT (("===" | "==" | "!==" | "!=") STRING)?A call argument is a STRING, a TEMPLATE, a bare name of a string parameter of the caller (or a string field of a struct answer, pick.pr), or a template call. A hand-off cannot be an argument of a call. Wherever a name is read (a condition, a switch subject, a call argument, ${}), it may be answer.field: one . and one level. A declaration never has a ..
Every argument has a name, as in Python: name=value. The name is a parameter of the callee: its scalar parameter, or a field of its struct parameter. The order of the arguments does not matter. A positional argument is a checker error, and no flag allows it. A parameter is given once.
Helpful errors from the parser:
| Input | Message |
|---|---|
for, while, do in a body | Rein has no loops yet |
let, var in a body, or const that is not an answer | Rein has no variables, except /** question */ const x: type = await request(...)`` |
import at the top of a file | Rein has no imports; reference functions with @./path and use std functions directly |
default in a switch | Rein has no default; add a case for every member |
else | Rein has no else: end the if block with a return, and handle the other case after it |
return; with no value | `return` needs a value: a string, a template, or `@./function` |
true or false in a condition or comparison | use the boolean parameter directly |
| a doc comment in any other place | the doc comment must go right before export default function, an interface, a parameter, a field, or a const answer |
x: Name or x: @./name as a parameter | a struct parameter is destructured: write `{ field, ... }: Name` |
| a doc comment before a struct parameter | a struct parameter has no doc comment; document each field in its `interface` |
await of a name other than request or a reference | only requestor a reference like@program can be awaited |
await outside a const answer, such as await @./x; or return await @./x; | `await` gives an answer; write `/** question */ const name: Type = await @program(...);`, then read `name` |
| a bare name as a return value | expected a string, a template literal, or a reference like @./path`` |
a second export default function | expected end of file ...; a file holds one export default function`` |
4. Declarations
Functions
export default function NAME(param) blockis the whole function. The file holds exactly one.export default async functionis an async function (section 4, Async requests).- The function doc comment sits right before
export default function. It gives the function description. It is required, and it must not be empty. There is no flag to skip this rule.rein check,rein eval,rein step,rein runand the MCP tools all refuse a function without one. - The function has zero parameters or one (section 4, Parameters). The parameter may end with a comma.
- The body is a block. It must return on every path (section 5).
- An empty body is an error:
function must return something. - A function has at most 40 lines. Its lines run from the
export defaultline to its closing}, both included. Every line counts, blank and comment lines too. The doc comment above the function and anyinterfaceoutside those lines do not count. - A program sets another limit with
maxFunctionLinesin itsrein.json: a positive integer, such as"maxFunctionLines": 60. A.struct.reinfile is not a function, so the limit does not apply to it.
Doc comments
Rein has four kinds of doc comment. Each one holds only its own text.
- The function doc comment. It sits right before
export default function. - A parameter doc comment. It sits right before the parameter, inside the parameter list. It gives the question the agent must answer for that parameter.
- A field doc comment. It sits right before the field, inside the
interface. It gives the question for that field, or the text the caller passes. A shared struct also has a struct doc comment, right beforeexport default interface. - An answer doc comment. It sits right before the
constof an async request (section 4, Async requests).
ts
interface Ticket {
/** What kind of ticket is it? */
category: "billing" | "bug" | "question";
/**
* Is the customer a VIP?
* Check the account tier in the CRM.
*/
vip: boolean;
}
/**
* Route a new support ticket to the right handling.
*/
export default function handleTicket({ category, vip }: Ticket) {
if (vip) {
return @./escalate;
}
return `Answer the ticket.`;
}Parsing rules, for each doc block:
- For each line, strip the leading whitespace, then one leading
*, then one following space if present. - The text before the first line that starts with
@is the description. It is trimmed. The lines of one paragraph are joined with one space, so a doc comment over several lines is one line of text. A blank line starts a new paragraph; a run of blank lines is one paragraph break. Example:/** Does the pull request wait for another pull request or change that is+ newline +* not merged yet? */is the descriptionDoes the pull request wait for another pull request or change that is not merged yet?. - Text after an
@line is not part of the description. - A
@tag is an error when it is@param:describe each parameter in its own doc comment, right before it. Any other tag gives a warning:unknown tag. - An empty function description is an error. An empty parameter doc comment is an error:
parameterxneeds a doc comment: /** question */ x: .... A field givesfieldxneeds a doc comment: /** question */ x: ....
Parameters
- A function has zero parameters or one. Two or more is an error:
a function takes one parameter; put `a` and `b` in a struct. - The parameter is a scalar or a struct.
- A scalar parameter is
name: type. The type isboolean, a union of strings, orstring. booleanis true or false. A union is one of its string members. It needs at least 2 unique, non-empty members. A leading|is allowed.stringis text. It is an input: the caller passes its value, and the agent is never asked for it (section 4, Inputs).- A struct parameter is destructured:
{ a, b }: Name. The braces name every field of the struct, each one once. The body reads each field by its name, as it reads a scalar parameter. - Errors on the braces:
`z` is not a field of `Name`; fields are a, b,destructure every field of `Name`; missing: `b`, and`a` is destructured twice. - In the rest of this reference, a parameter is the scalar parameter or a field of the struct parameter. Its name is unique within the function. Answer keys,
--set,--input,rein describeand the graph use the field names.
Structs
A struct is a TypeScript interface: a list of fields. A field is like a scalar parameter: a doc comment, a name and a type (boolean, string, or a union). A field ends with ;, ,, or nothing. A field cannot be a struct. Rein has one struct form, interface. It has no type X = { ... }.
A struct is inline or shared.
- An inline struct is an
interfacein the file of the function. It is the input of that function only. No other file can name it. - An inline struct cannot be exported. Error:
input struct `Input` cannot be exported; to share it, move it to its own `.struct.rein` file. - The function names its inline struct:
({ a, b }: Input). A missing one is an error:cannot find struct `Input`; declare `interface Input { ... }` in this file. - An inline struct that the function does not take gives a warning:
unused struct `X`; an inline struct is the input of the function in its file. - A shared struct is a file
<name>.struct.rein. It holds oneexport default interfaceand nothing else. Error:a `.struct.rein` file holds one `export default interface` and nothing else. - A shared struct needs a doc comment:
missing doc comment on struct `Name`; put /** description */ right before `export default interface`. - A function names a shared struct with a reference:
({ a, b }: @./message)names the struct ofmessage.struct.reinin the same folder. The reference rules apply (section 4, References): the path is relative, has no extension, and stays inside the root. A missing file is an error:cannot find struct `@./message`; a shared struct is a `message.struct.rein` file. - When two functions take the same struct, use a shared struct. An inline struct belongs to one function.
- A function reference, such as
return @./message;, never names a.struct.reinfile.
Example: message.struct.rein, a shared struct that two templates take.
ts
/**
* One sentence for the customer: what happened, then what comes next.
*/
export default interface Message {
/** The first part of the sentence, such as the action taken. */
opening: string;
/** The next step the customer can expect. */
next_step: string;
}refund/tell_customer.rein takes it. faq/close_ticket.rein takes it the same way.
ts
/**
* Tell the customer what happens next, in one sentence.
*/
export default function tellCustomer({ opening, next_step }: @../message) {
return `${opening} ${next_step}`;
}A caller gives each field by name: @./refund/tell_customer(opening=`...`, next_step=`...`).
References
A relative reference names another function of the project. The path is relative to the file that holds it, and it has no extension.
@./nameis a function in the same folder.@./ci/fixisci/fixin the same folder.@../escalateisescalateone folder up.- The reference resolves to
<dir of current file>/<path>.rein. The target must stay inside the project root.@../../outsideinci/fix.reinis an error:`@../../outside` leaves the project root. - A missing target is an error on the reference:
cannot find function `@../x`. - Function identity is the root-relative name (
ci/fix). The CLI, JSON, graph, cycles, and reachability frommainuse it. return @./path;hands the agent to the function (section 9). The function must have nostringparameter.return @./path(name=value);calls a template (section 4, Templates). It hands the agent to a normal function with values (section 4, Inputs).const name: Type = await @./path(name=value);runs a function and waits for its result (section 4, Calls with return).@program/functionhands off to a function of another installed program. The program is found by name through the scopes, asrein listandrein step NAMEfind it. A bare@programmeans the entry of that program. It is allowed as a hand-off in areturn, or as the target of anawait(section 4, Calls with return), with the same argument rules as a hand-off to a local function: onlystringparameters are passed, by name. The other inputs of the target are asked by the target as normal questions.- A program that is not installed is an error on the reference:
unknown program `x`; run `rein list` to list programs, or `rein install -g x` to install it. A function that the program does not have is an error:cannot find function `f` in program `x`. - A program with a
package.jsonreaches only the programs it declares there, as pnpm gives a package only its declared packages. Itsdependencies,devDependencies,optionalDependenciesandpeerDependenciescount. A reference to any other program is an error on the reference:program `x` is not a dependency of `a`; add it to `dependencies` in package.json, such as with `pnpm add x`. The rule holds inside a linked program too, against that program's ownpackage.json. A folder without apackage.jsonis not a package, so it may hand off to any program. - Rein loads every program that a hand-off can reach, through other programs too, when it loads the project. A program reference that a hand-off reaches inside another program, and that does not resolve, is an error on the function of this project that starts the hand-off, at 1:1:
`@b/main` hands off to `@c/main`: unknown program `c`; run `rein list` to list programs, or `rein install -g c` to install it. Sorein checkreports it, and the first step ofrein step,rein runandrein_start, and thefirst_stepofrein_programs, is an error step. The run does not start, and the agent learns about each missing program before it does any work. rein stepfollows the hand-off into the other program in the same run. The functions of that program have the name@program/function, so its answer keys are@pr-handling/main.task, and a local filepr-handling/main.reincannot collide with them. The trace shows the same names. A program reference inside the other program is followed the same way, so a chain of hand-offs can cross many programs.rein graphdraws aprogramnode labelled@program/function, and a hand-off edge to it. It does not draw the other program.- A reference to a template without a call is an error:
`@./tpl` is a template; call it with arguments, like `@./tpl(...)`.
Std
The std library needs no import. Its names are always available: delegate, askUser, and request.
| Name | Kind | Signature | Text |
|---|---|---|---|
delegate | template | delegate({ model, task }: Input) | "Delegate this task to a subagent.\nModel: ${model}\nTask: ${task}". Doc: "Hand a task to a subagent, with the model to use." Its input is an inline struct. Fields: model, the model the subagent runs on; task, what the subagent does. A call is delegate(model="...", task="..."). |
askUser | template | askUser(question: string) | "Ask the user: ${question}". Doc: "Ask the user a question and wait for the answer." Param: question, the question in the user's language. |
request | built in | await request(value) | Asks the agent to do a step, then wait for its answer (section 4, Async requests). It is not a template. |
An unknown std name is an error that lists the std names: `x` is not in std; std has: askUser, delegate, request.
Templates
A template is a file whose function meets every rule below. It is like a Rust macro: it expands to a string. Any other function is a normal function. A normal function may mix string fields with boolean and union fields in its struct.
- It has a parameter. The parameter is a
string, or a struct whose fields are allstring. Each one has its own doc comment. - The body is exactly one
returnof a template literal. There is noif, noswitch, no import, and no other statement. - Every parameter is used in the template literal:
template parameter `x` is not used; add ${x} to the template. - A template is called from a function, by a std name (
delegate(...)) or a reference (@./templates/reply(...)). - A template call is a value, not a step. It adds no hop to the
rein steptrace. - Call arguments are named (
name=value) and their values are strings, template literals, bare names of astringparameter of the caller, or template calls. A template literal argument may interpolate a union parameter or a union answer of the caller. A boolean cannot be interpolated. - Every parameter is given once, by name:
missing argument `b` in call to `@./tpl`; write `b=value`. - A template is never run by itself:
rein eval <template>is an error,templates are called from functions, not run.
Section 4, Structs, shows a template, refund/tell_customer.rein, that takes a shared struct.
Inputs
A string parameter of a normal function is an input. The caller passes its value. The agent is never asked for it.
- An input is used only in a template literal, as
${name}, or as a bare name argument of a call. - A condition or a switch subject cannot read an input. Error:
`pr` is a string input; a condition cannot read it. return @./merge(pr=pr);hands the agent to a normal function and passes the value. The arguments bind by name to the target'sstringparameters.- An argument is
name=value, where the value is a STRING, a template literal, or a bare name of astringparameter of the caller. Any other bare name is an error:`x` is not a string input. - Every
stringparameter of the target is given by name, once.return @./merge;to that function is an error:`@./merge` takes 1 argument, got 0. - Arguments to a function with no
stringparameter are an error:`@./x` takes no arguments; return it without a call.
Example, main.rein and merge.rein. main passes pr to merge. merge uses it in a template literal. merge also asks the agent for ci, a union, so it is a normal function.
ts
/**
* Merge the pull request when it is ready.
*/
export default function main(
/** The pull request link. */
pr: string,
) {
return @./merge(pr=pr);
}ts
interface Input {
/** The pull request link. */
pr: string;
/** What is the CI status of the PR? */
ci: "green" | "red" | "pending";
}
/**
* Merge the pull request when CI is green.
*/
export default function merge({ pr, ci }: Input) {
if (ci === "green") {
return `Merge ${pr}. Then delete its branch.`;
}
return `Wait for CI, then start this program again.`;
}Async requests
A function can ask the agent for a step and wait for an answer. The answer is a typed const:
ts
/** Answer a FAQ ticket with a help article, or escalate it. */
export default async function answerFaq(
/** Did you find a help article that answers the question? */
article_found: boolean,
) {
if (!article_found) {
return @../escalate;
}
/** Did the customer confirm the article solved the problem? */
const solved: boolean = await request(
`Reply with the help article link and ask the customer if it solved the problem.`,
);
if (solved) {
return `Close the ticket as solved.`;
}
return @../escalate;
}- The
constmust have a doc comment, a type (boolean, a union of strings, or a struct; notstring),= await request(value), and an optional;. A barestringis an error:a `string` answer is not allowed; answer a struct with a `string` field. valueis a string, a template literal, or a template call.await request(delegate(model=`use opus model`, task=`code review the pr`))is valid.- The
constis the onlyconstin Rein. It is an answer, not a variable. Its name is a parameter for the rest of its block. A name clash with a parameter or another answer is an error:duplicate name. Using the name before its declaration is an error:`x` is used before it is declared. awaitneedsexport default async function. Anasyncfunction with noawaitis a warning:`async` function has no `await`; drop `async`.- An answer is never free text. To get a value such as a link, answer a struct with a
stringfield (next section). - The answer is stateless. When the run reaches the
await, and no value is given for the answer, the run stops with a request (section 9).
Struct answers
An answer can be a struct. The struct is an inline interface in the file, or a shared @./name (name.struct.rein). Section 4, Structs, gives the struct rules.
ts
interface Pick {
/** The link of the pull request. */
pr: string;
/** Which task pr-handling runs. */
task: "review" | "address comments" | "fix ci" | "merge";
}
/** Pick the next pull request notification and hand it off. */
export default async function pullRequest() {
/** The pull request of the next notification. */
const pick: Pick = await request(`Pick the next pull request notification.`);
return @pr-handling(pr=pick.pr);
}- Only a
requestanswer can be a struct. A parameter cannot. - Every field is
boolean, a union, orstring, and has a doc comment (the struct rules of section 4). Astringfield is allowed because the struct gives the value a name, a doc comment and a schema. pick.prreads one field. It is one level:a.b.cis an error. The field is in scope as long as the answer is.- A
stringfield is used where astringparameter is used: a hand-off or template argument (@./merge(pr=pick.pr)) and${pick.pr}in a template literal. Abooleanor union field is used like a boolean or union answer: in a condition or in aswitch(switch (pick.task)). - Errors:
`z` is not a field of `Pick`; fields are pr, task,`pick.pr` is a string input; a condition cannot read it, and`pick` is a struct answer; read one of its fields, like `pick.field`. rein stepasks for the struct as a JSON object (section 11).
Calls with return
A function can run another function and use its result as a value. The answer is a const, as for request. The value after await is a reference:
ts
interface Projects {
/**
* The high priority repositories (`owner/name`), comma-separated,
* or `-`.
*/
repos: string;
/** Did priority-projects find no repository? */
none: boolean;
}
/** Work on the high priority projects of a workplace. */
export default async function work(
/** The workplace folder that lists the projects. */
workplace: string,
) {
/** The high priority repositories. */
const projects: Projects = await @priority-projects(workplace=workplace);
if (projects.none) {
return `No priority project has activity.`;
}
return @./work/open_prs(repos=projects.repos);
}- The forms are
await REF(args)andawait REFwith no arguments. REF is any hand-off target:@./path,@../path,@program, or@program/function. The argument rules are the rules of a hand-off (section 4, Inputs). Everystringparameter of the target is given by name, once. A target with nostringparameter takes no arguments. - The doc comment and type rules are the rules of a
requestanswer (section 4, Async requests): a doc comment, and aboolean, union or struct type.awaitneedsexport default async function. - The target must be a function. A template gives text, not a run:
`@./tpl` is a template; only a function can be awaited. A target that does not resolve gives the errors of a hand-off. A program target follows thepackage.jsonrule (section 4, References). await REFoutside aconstanswer is an error:`await` gives an answer; write `/** question */ const name: Type = await @program(...);`, then read `name`.rein stepwalks the target as a hand-off. Its questions and requests have the target's keys after the caller's answer key and>, such aswork.projects>@priority-projects/main.has_list(section 11). Each call has its own keys, so two calls of one function, or each level of a recursive call, ask their questions again. A hand-off inside it is followed. When the target ends with text, the run does not end: the next step is a request for theconst(section 11). When theconsthas an answer, the function goes on after theawait.- A target may itself use
await. The innermostawaitis answered first.
5. Statements
if (cond) { ... }is a guard. Its block must return on every path. Otherwise the error iseveryifblock must end in a return.- There is no
else. The code after anifhandles the other case. Guard chains are the form: check a case, return, and continue. switch (name) { ... }picks one group ofcaselabels by the value of a union parameter or answer.- Every member of the union needs a case. Otherwise the error is
switch onxneeds a case for every member; missing:y``. - There is no
default. - Labels with no body join the next body:
case "a": case "b": return x;is one group. - A clause body that has statements must end in a return. Otherwise the error is
case falls through; end it with return.
- Every member of the union needs a case. Otherwise the error is
return `text`;,returntext ${x};,return @./fn;, or a call ends the function. The;is optional.- A statement after a
return, or after aswitchthat always returns, is an error:unreachable statement. - A function must return on every path. A body that can end without a return is an error:
every path must return something; end the function with a return. - A function returns something on every path: a string, a template, or a reference to another function.
return;with no value is an error.
6. Conditions
- A boolean parameter or answer is used bare:
if (docs_only), or negated:if (!docs_only). - A union parameter or answer is compared with
===or!==:if (ci === "green"). - A
stringparameter, or astringfield of a struct answer, cannot be read by a condition or a switch (section 4, Inputs). Abooleanor union field of a struct answer (pick.task) is used like a boolean or union answer. !,&&,||combine conditions.!binds tightest, then&&, then||. Parentheses group.- Evaluation of
&&and||stops early when the result is known.
7. Values
- Boolean values:
true,false,yes,noon the command line.yesandtruemean true.noandfalsemean false. Case does not matter. - Union values: one of the declared members, as a string.
- Return values: a STRING, a TEMPLATE, a reference, or a template call.
8. Checker rules
When a file has a syntax error, only that error is reported for the file. It is the first syntax error. The other checks run after the syntax error is fixed.
Errors
| Rule |
|---|
A quoted string in a text position (text uses backticks: write `...` instead of "..."). Text positions are listed in section 2, String kinds |
A template in a tag position (a tag uses quotes: write "green" instead of `green`). It is a syntax error. A template with ${} or a newline shows ... |
A function file name has a / part that is not an identifier, or is a keyword or reserved word (invalid function file name) |
The file has no function doc comment (missing doc comment on NAME; put /** description */ right before export default function``). A // line comment does not count. |
The function description is empty (empty description on NAME; write the function description in the doc comment). A doc block with only @ tags is empty. |
| A parameter has no doc comment or an empty one |
A doc comment has @param |
The function body is empty (function must return something) |
A function has more lines than the limit (function `NAME` has N lines; the limit is M. Move part of it into its own function and call it with `@./name`). It points at the header, export default function NAME |
rein.json sets maxFunctionLines to a value that is not a positive integer (`maxFunctionLines` in rein.json must be a positive integer, such as 40; found VALUE). It points at rein.json, and the limit stays 40 |
A line has more characters than the limit (line has N characters; the limit is M. Break the line; a long text can be a template (backticks) that spans lines). It points at the part of the line after character M |
rein.json sets maxLineLength to a value that is not a positive integer (`maxLineLength` in rein.json must be a positive integer, such as 80; found VALUE). It points at rein.json, and the limit stays 80 |
| Two parameters share a name |
A function has two or more parameters (a function takes one parameter; put `a` and `b` in a struct) |
| A struct parameter names a field that its struct does not have, leaves a field out, or names a field twice |
| A struct parameter names an inline struct that the file does not declare, or a shared struct that does not resolve |
| An inline struct is exported |
A field has no doc comment (field `x` needs a doc comment: /** question */ x: ...), two fields share a name, or a field union is not valid |
A .struct.rein file holds a function, a second struct, or a struct that is not export default |
| A shared struct has no doc comment, or an empty one |
A .struct.rein file name has a / part that is not an identifier (invalid struct file name) |
| A union has fewer than 2 members |
| A union has a duplicate or an empty member |
| A condition uses a union parameter bare |
A condition compares a boolean parameter with === or !== |
| A condition or switch subject names an unknown parameter |
A condition or switch subject names a string parameter (`pr` is a string input; a condition cannot read it) |
A bare name in an argument is not a string parameter of the caller (`x` is not a string input) |
switch subject is not a union |
| A compared value or case value is not a member of the union |
| A switch does not have a case for every member |
| Two case labels use the same value |
A case body does not end in a return (case falls through; end it with return) |
An if block does not end in a return (every if block must end in a return) |
A path through the function ends without a return (every path must return something) |
A statement follows a terminating statement (unreachable statement) |
A reference does not resolve (cannot find function `@./x`), or leaves the root (`@../../x` leaves the project root) |
A program reference names a program that is not installed (cannot find program `x`; run `rein list` to list programs), or a function the program does not have (cannot find function `f` in program `x`) |
| A reference to a template is returned without a call |
A call, a hand-off or an await has a positional argument (missing argument `a` in call to `@./tpl`; write `a=value`) |
A call, a hand-off or an await names an argument that is not a parameter (`c` is not a parameter of `@./tpl`; parameters are a, b) |
A call, a hand-off or an await gives one parameter twice (`a` is given twice in call to `@./tpl`; give each name once) |
A call, a hand-off or an await leaves a parameter out (missing argument `b` in call to `@./tpl`; write `b=value`) |
A call, a hand-off or an await gives arguments to a function with no string parameter (`@./x` takes no arguments; return it without a call) |
A call to a function that is not a template is not the value of a return, for example inside an argument or a request value (A hand-off cannot be an argument of a call.) |
| A std name is unknown, or a call names a std item that is not a template |
A template has an if, a switch, an import, or a statement other than one template return |
| A template parameter is not used |
| A template interpolates a boolean, or a name that is not a parameter or an answer |
An import statement is in the file (Rein has no imports) |
await without export default async function |
await of a name other than request or a reference (only requestor a reference like@program can be awaited) |
await outside a const answer (`await` gives an answer; write `/** question */ const name: Type = await @program(...);`, then read `name`) |
An await names a template (`@./tpl` is a template; only a function can be awaited). A target that does not resolve, or a wrong argument, gives the hand-off errors above |
| An answer has no doc comment |
An answer is a string (a `string` answer is not allowed; answer a struct with a `string` field) |
A struct answer names a struct that does not exist (cannot find struct `X`) |
A field of a struct answer is not in the struct (`z` is not a field of `Pick`; fields are pr, task), or the answer itself is read as a value (`pick` is a struct answer; read one of its fields, like `pick.field`) |
An answer name clashes with a parameter or another answer (duplicate name) |
An answer is used before its declaration (`x` is used before it is declared) |
The flow root has no main.rein (project has no main.rein; it is the agent's entry point). It is reported on main.rein, at 1:1. A step reports it only when its entry is missing too |
A hand-off reaches, inside another program, a program that is not installed or a function that the program does not have (`@b/main` hands off to `@c/main`: unknown program `c`; run `rein list` to list programs, or `rein install -g c` to install it). It is reported on the function of this project that starts the hand-off, at 1:1 |
Warnings
| Rule |
|---|
| A parameter is declared but never used |
An inline struct is not the parameter of the function in its file (unused struct) |
An async function has no await |
A reference cycle, such as a -> b -> a. A cycle acts as a loop. The checker gives one warning on every return or await of a reference that closes the cycle. The one exception: a cycle with an await call on it is not warned when a question comes before one of its edges (a parameter the agent answers, or a request answer above the call). Each level then asks a new key, and the 64-step limit still stops endless recursion. An await with no question before it on the cycle, such as const again: boolean = await @./a; as the first step, is warned, since it fails at the 64-step limit |
A doc tag other than @param (unknown tag @x``) |
A function cannot be reached from the entry (main, or entry in rein.json) through references (x is not reachable from main). This warning is not given when the entry is missing or has a syntax error |
A function that exactly one other function calls is not in the folder named after that caller (`fix_ci` is only called by `merge`; move it to `merge/fix_ci.rein` so the folders show the flow. The move changes its answer keys and breaks `@<program>/fix_ci` references from other programs). A function that only the entry calls belongs at the root. It is not given when the entry is missing or has a syntax error, since reachability is unknown. Section 1 has the rule and its exceptions |
An if has no empty line above it, unless it is the first statement of its block (add an empty line before if``). It points at the if keyword. Section 2 has the rule |
An if block has no empty line after its closing }, unless the next non-empty line is a } (add an empty line after the if block). It points at the }. Section 2 has the rule |
A parameter, struct field or answer name holds an uppercase letter (`isDraft` is camelCase; names are snake_case: write `is_draft`) |
9. Evaluation
- One run evaluates one function. By default that is
main. - The runtime asks for a parameter only when the taken path needs it. This is lazy evaluation.
- A run returns one of:
- a STRING: the agent does what the text says. A template or a template call expands to a STRING.
- a REF: the agent runs the target function next, using its description and inputs. The REF is the target's project path, such as
ci/fix. - a request for a parameter: the agent answers the question, then runs again.
- a request for the agent to act: the agent does the instruction, then answers the
const, then runs again. - a call with return: the agent runs the target to its end, then answers the
constwith the result (section 4, Calls with return).rein evalprints it as a request with the instructionRun `<target>` to its end. Its final instruction is the result.
- Parameter values come from
--set name=value. A boolean takestrue,false,yes, orno. A union takes one of its members. Astringparameter takes any text, and it is never a question. Setting one name twice is an error:inputnameset twice. --setalso gives an answer its value. An answer's value is checked when the run reaches itsconst.- A template run by itself is an error.
- A hand-off with arguments carries their values, by name, to the target. The agent runs the target with them (section 4, Inputs).
10. CLI
| Command | Purpose |
|---|---|
rein check [PATH] | Check a project directory, a directory inside a project, or one .rein file. Default .. Prints file:line:col: error: message for the files under PATH. Exit code 1 when there are errors, when PATH does not exist, or when PATH is not a .rein file. |
rein eval [FUNCTION] [--root DIR] [--set name=value]... [--json] | Evaluate one step. FUNCTION defaults to main. Prints the returned string, or next: target with the target's description and inputs. Exit code 2 when a parameter is needed. Exit code 3 when the agent must do a request. --set also gives a value to a string parameter. A name given twice with --set is an error. |
rein run [NAME] [--root DIR] [--entry FN] [--json] | Run a program as a live process. Writes one step per line, reads one answer per line on stdin. See interface.md. |
rein describe [FUNCTION] [--root DIR] | Print the function's schema as JSON. FUNCTION defaults to main. Usable as an agent tool schema. A template's parameters have type string. |
rein graph [NAME | --root DIR] [--refs] [--entry FN] [--input name=value]... [--answer key=value]... [--format mermaid|dot|json|html] [-o FILE] [--open] | Print the program graph. NAME is a program name (section 1a) or a .rein file. The default is the project around the current directory. The default view is the decision flow: one node per function, per question (its text, then name: type), per request, and per outcome (done text or delegate). An edge from a question has the answer as its label: yes, no, or union members. A hand-off edge has its named arguments. A dashed edge goes to a template that a call uses. --refs draws only one node per function and one edge per reference. Node ids are f0, f1, ... for the functions in name order, and f<i>_<j> for the steps of function i in source order, so the output is stable. The entry is drawn as a stadium shape. --format is mermaid (default), dot (Graphviz), json, or html. -o FILE writes to FILE. A .svg or .png FILE is rendered from DOT by Graphviz dot; when dot is not on PATH, the command fails and names it. JSON: {version, view, entry, nodes: [{id, kind, function, label, entry?, name?, type?}], edges: [{from, to, kind, label?, args?: [{name, value}], taken?}], path?, current?}. Node kinds: function, question, condition, request, done, delegate, program (a function of another program), unresolved. Edge kinds: flow, handoff, template, call. An await @target is a request node labelled await @target and name: type. A call edge goes from it to the target's node (a program node for another program), with the named arguments. Mermaid draws it thick (==>), DOT bold. The flow goes on from the await node. The graph shows functions, not calls. When a walk visits one function in several calls, its nodes are marked with the answers of its first call, in the order rein step walks (section 11). The rein watch graph follows the same rule. With --entry, --input or --answer, Rein first steps the flow with those values, as rein step does, with the same checks, error texts and exit code 1 on an error. The graph then marks the walk: the nodes and edges taken, the node where the walk stops (the next question, a request, or the outcome), and the rest dimmed. A missing answer stops the walk at its question. Mermaid uses the classes taken, current and dimmed; DOT uses colors and pen widths. JSON adds path (the ids of the nodes taken, in order), current: {node, kind, key?} (kind as the step response) and taken: true on each edge taken. The schema version stays 1: the fields are new and absent without a walk. html writes one page that draws the Mermaid diagram with mermaid 11.4.1 from cdn.jsdelivr.net, with a legend and the inputs and answers used. A .html FILE, or --open, picks html when --format is not given. --open opens the page in the default browser (open on macOS, xdg-open on Linux, nothing elsewhere). Without -o, it writes the page to a temporary file. |
rein debug [NAME | --root DIR] [--entry FN] [--input name=value]... [--answer key=value]... [--port N] [--no-open] | Debug a program in the browser. Serves one page on 127.0.0.1 (a free port unless --port), prints its URL, and opens it unless --no-open. Ctrl-C stops it. --entry, --input and --answer are optional starting values. A string input left unset is walked as the placeholder <name>, so the page always shows a step. The page can switch the entry. The page has one control per string input of the entry and per key the flow can ask for: yes/no, a list of the union members, or a text box (a struct answer is JSON). Empty is unset. On each change the page sends a step request to POST /walk. The server runs the step engine and answers {step, mermaid, path, current, keys, placeholders}: the step response, the graph with the walk marked as rein graph --answer marks it, the answer key of each question node, and the inputs walked as placeholders. GET /schema lists the controls: {entry, functions, inputs: {function: [{name, description}]}, answers: [{key, type, options, fields, description}]}. The server answers only a request whose Host is 127.0.0.1:PORT or localhost:PORT. |
rein watch [--port N] [--no-open] | Serve one page on 127.0.0.1 that lists the agents using rein now. Counter chips show the agents alive, and the newest run of each shown agent as waiting, done or error. Each agent has one card with its transport, cwd, session and pid. Each run has a status chip, its path as pills (the last pill is outlined while the run waits or fails), and a "now" line with the step it waits on. The newest run has a flow graph that marks the taken path and the "now" node. Its "Path" view shows only the functions on the path. Its "Whole program" view shows all functions. If the program changed since the run, the page shows "Program changed since this run" instead of the graph. Long text (instruction, question, answers, inputs, error, log lines) and earlier runs start closed. The page reads the run logs in $REIN_RUNS_DIR, else $XDG_STATE_HOME/rein/runs, else ~/.local/state/rein/runs, and polls them every 2 s. Ended agents are hidden unless the page asks for them. It writes nothing. The server answers only a request whose Host is 127.0.0.1:PORT or localhost:PORT. Ctrl-C stops it. |
rein step [--root DIR | --flow NAME] [--entry FN] [--answer key=value]... [--input name=value]... [--stdin] [--json] | Step the whole flow from its entry (entry in rein.json, else main, as rein check uses it). --flow picks a flow by name (section 1a); --root and --flow exclude each other. --input gives a value to a string parameter of the entry function. The agent-facing interface (section 11). |
rein list [-l] [-1] [-a] [-r] [-t] [--json] [PATTERN...] | List the programs visible from here, as ls lists files. Names by default; -l for scope, version, path and description; -a for shadowed programs; PATTERN is a name glob. rein flows is an alias. |
rein show <NAME> | Print a program's manifest and every .rein file, entry first. |
rein serve --mcp | The MCP host. Holds live runs in memory. Tools: rein_programs, rein_show, rein_start, rein_answer, rein_status, rein_stop, rein_list. See interface.md. |
rein agent-context [--format text|json] | Print the block an agent needs to use the visible flows (section 12). Prints nothing when there are none. |
rein hook [--caller NAME] | The agent hook entry point (section 12). Reads one event on stdin. |
rein install [-g] <args>... | Run pnpm add --ignore-scripts <args> in the npm folder: the repo's rein/npm/, or the user's with -g. See Install a program. |
rein uninstall [-g] <args>... | Run pnpm remove <args> in the same folder rules. |
rein update [-g] [<args>...] | Run pnpm update --ignore-scripts <args> in the same folder rules. |
rein lsp | Run the language server over stdio. See Language server for its features. |
rein dap | Run the debug adapter over stdio (Debug Adapter Protocol). See Debug adapter. |
Exit codes: 0 for a text or reference result, 1 for an error, 2 for a parameter needed, 3 for a request.
Columns are 1-based and count Unicode characters. The language server counts character positions in UTF-16 code units, as the LSP protocol requires.
Input request output
Text form:
text
need input <name>: <type> - <description><type> is boolean, string, or a union as TypeScript writes it, such as "green" | "red".
Request output
Text form, from rein eval when the agent must act (exit code 3):
text
request: <instruction>
then answer: <name>: <type> - <description>
hint: rerun with --set <name>=<value>Reference output
Text form, from rein eval when the next step is another function:
text
next: <function>
<description>
<name>: <type> - <description>rein eval --json
Each run prints one of these objects:
json
{"kind":"text","value":"..."}
{"kind":"ref","function":"...","description":"...","inputs":[...]}
{"kind":"need_input","input":{"name":"...","type":"boolean|union|string","options":["..."],"description":"..."}}
{"kind":"request","instruction":"...","response":{"name":"...","type":"boolean|union","options":["..."],"description":"..."}}options appears only for union inputs. A boolean input has "type":"boolean". A union input has "type":"union", and its members are in options.
rein describe output
json
{
"name": "merge_pr",
"description": "...",
"inputs": [
{"name": "docs_only", "type": "boolean", "description": "..."},
{"name": "ci", "type": "union", "options": ["green", "red", "pending"], "description": "..."}
]
}Language server
The code is in crates/rein/src/lsp/, one module per feature. It reads the tree from rein_core's parser and resolves references with rein_core::project. A relative reference cannot leave its program, so every feature below stays in one program, except workspace symbols and program references.
- Diagnostics: the checker's errors and warnings, on each open file.
- References: on a reference, on a function's name, or anywhere else in a file, every
@./reference to that function. On a parameter or aname=argument, its uses andname=in each call to its function. On a struct type or a struct name, every parameter type that names the struct. On a field, its place in{ }and its uses in each function that takes the struct, andname=in each call to those functions. The declaration is included when the client asks for it. - Code lens:
N referencesabove each function, and above a shared struct the count of parameter types that name it. Its command,rein.showReferences, carries the file URI, the position, and the locations. - Go to definition: a reference goes to the name of its function. A parameter or
name=goes to the parameter. A struct type goes to itsinterface, in the same file or in its.struct.reinfile. A field in{ }, in the body, or asname=goes to the field in itsinterface. A std template goes to its source, which the server writes torein-std/in the temp directory.requestis built in, so it has no source. - Call hierarchy: incoming calls are the functions that reference the function, outgoing calls are the functions it references. Each call has the spans of its references.
- Hover: on a reference or a function's name, the description and the parameters. On a parameter, a field or
name=, its type and question. On a struct type or a struct name, its doc and its fields. On a std name, its doc. - Rename: a parameter renames its declaration, its uses, and
name=in each call. A field renames it in itsinterface, in{ }and the body of each function that takes the struct, andname=in each call. A function renames its file in the same folder, its folder of nested functions when it has one, the last segment of each reference, each relative reference that the folder move changes, and the name afterexport default functionwhen it was the file name. An inline struct renames itsinterfaceand the parameter type. A shared struct renames its file to<new>.struct.rein, the last segment of each@./type, and itsinterfacename when it was the file name. A function or shared struct rename needs a client that can rename files. - Document symbols: the function, with its parameters and
constanswers, then each struct with its fields. - Workspace symbols: every function of every program under the workspace folders and of the open files. A function's name is its project path. A shared struct is a struct symbol, named by its path without
.struct. Its container is the program folder. - Completion: after
@./or@../, the project functions relative to the current file, or after}:the shared structs. Each item replaces the whole reference token. Inside call parentheses, where an argument name goes, the parameters (the fields of a struct parameter) not given yet, asname=. Afterx === "orcase ", the members of the union, each replacing the whole literal. Elsewhere, the parameter names, the std names, and keywords. - Signature help: inside call parentheses, the callee's parameters (the fields of a struct parameter), with the current one active.
- Quick fixes: add a doc comment to a function or a parameter that has none, and add
name=to a positional argument. - Program references:
@programand@program/functionresolve through the scopes from the project root, asrein checkrun there resolves them. Go to definition opens the function in the installed program, or its entry for a bare@program. Hover adds the program name, version, scope and folder. Completion after@offers the visible programs, and after@program/the functions of that program. Signature help,name=completion and diagnostics read the target's parameters. References list the program references in the current program only; references from other programs are not searched. Rename refuses a symbol of another program. The scopes are searched only when the project has a@programreference, and a program is loaded once for the request. An open buffer of an installed program replaces its file on disk, so hover, completion and diagnostics in a hand-off see its unsaved edits. This holds when the program's folder is reached through a symlink, as pnpm linksnode_modules/<name>. An open buffer the loader would skip, such as one in a nested flow, a hidden folder,target/ornode_modules/, is not a function of the program. Such a file gets no language features. Its one diagnostic is a hint at its top, which names the folder the loader does not load. Only an open buffer gets the hint: it clears when the buffer closes or the file moves. A file of an installed program, which is a folder inside the skipped folder with arein.jsonor amain.rein(such asnode_modules/<name>/or pnpm'snode_modules/.pnpm/<pkg>/node_modules/<name>/), is a program of its own and gets no hint. Completion after@./or@../searches no scope. After@program/a program the file already references is read from its loaded copy.
Debug adapter
The code is in crates/rein/src/dap/. rein dap speaks the Debug Adapter Protocol over stdio. It walks a program with the step engine (section 11), so it has no evaluator of its own. It stops at a line breakpoint on a question, a request, a hand-off, or the end. A question with no answer is sent to the client as a rein/ask event. The client answers with a rein/answer request, and the walk goes on. The editor side and the steps for the user are in the VS Code extension README.
11. Stepping (agent interface)
rein step steps the whole flow in one call. It is the interface for agents. rein eval evaluates one function, and is the debug command. Live runs (rein run, rein serve --mcp) call the same logic and keep the answers above it; see interface.md. The stepping logic lives in rein_core::step. It is pure: no I/O and no state. The CLI only parses flags, calls it, and prints.
Protocol version 1. A request and a response are JSON objects, each with a version field. A daemon or another transport can carry the same objects unchanged.
Request
json
{"version": 1, "entry": "main", "answers": {"main.vip": "no"}}json
{"version": 1, "flow": "support-ticket", "answers": {"main.vip": "no"}}json
{"version": 1, "entry": "main", "inputs": {"pr": "https://example.com/pr/1"}, "answers": {}}version: default1. Another value is an error. Adding the optionalflowandinputsfields did not change the version.flow: optional. The name of a visible flow (section 1a). The CLI resolves it to a flow root and steps that root. Giveflowor a root, not both.entry: the function to start from, by its path. Defaultmain.inputs: optional. An object. Each key is the name of astringparameter of the entry function, and each value is its text. Default{}.answers: the answers so far. Each key is<function path>.<name>, wherenameis a parameter or aconstanswer, such asmerge_pr.ciorfaq.solved. A function of another program has the path@program/function, such as@pr-handling/main.task. Inside a call with return, a key starts with the caller's answer key and>. Whenmainrunsconst b: Found = await @./fn(...), the answers offnin that call aremain.b>fn.ok, and of another programmain.b>@pr-handling/main.draft. Calls nest:main.b>fn.c>leaf.ok. A hand-off adds no prefix of its own: outside any call its keys are as before, and inside a call it keeps the call's prefix, such asmain.b>g.okwhenfnreturns@./g. Each part before a>must be a call that a walk can make:<function>.<const>, anawaitwhose target reaches the next function through hand-offs. Any other key is unknown, such asmain.b>leaf.okwhenleafis only awaited byfn. Each value is as--answertakes it:yes,no, or a union member. The value of a struct answer is a JSON object, such as{"pr": "https://example.com/pr/1", "task": "review"}: with--stdinit is a real JSON object, and with--answerit is the JSON text (--answer main.pick='{"pr":"https://example.com/pr/1","task":"review"}'). The runtime checks it: it is an object, every field is present, no other field is, astringfield is non-empty text, abooleanfield istrueorfalse, and a union field is a member. An error names the field, such asfield `pick.pr` is empty; give some text. Responses echo a struct answer as a JSON object. Default{}.
Response
kind is one of four values. Every response has version, answers (the answers the request gave, echoed back), and trace (the function paths visited, in order).
kind | Fields | Meaning | Exit code |
|---|---|---|---|
question | key, function, name, type (boolean or union), options (union members, else null), description | Answer key, then step again. | 2 |
request | key, function, instruction, response (name, type (boolean, union or struct), options, description, and for a struct fields) | Do instruction, then answer key, then step again. | 3 |
done | instruction | The flow ended. Do instruction. | 0 |
error | message, key (or null), diagnostics (file, line, column, message) | The request is not valid. Fix it and step again. | 1 |
For a struct answer, response.type is struct, options is null, and response.fields lists the fields of the struct, in order. Each field is {name, type, options, description}: type is boolean, union or string, options lists the members of a union (else null), and description is the field doc comment. The answer is a JSON object with exactly these fields.
json
{"kind": "request", "version": 1, "key": "main.pick", "function": "main", "instruction": "Pick the next pull request notification.",
"response": {"name": "pick", "type": "struct", "options": null, "description": "The pull request of the next notification.",
"fields": [
{"name": "pr", "type": "string", "options": null, "description": "The link of the pull request."},
{"name": "task", "type": "union", "options": ["review", "address comments", "fix ci", "merge"], "description": "Which task pr-handling runs."}]},
"answers": {}, "trace": ["main"]}The answer for that request, in answers:
json
{"main.pick": {"pr": "https://example.com/pr/1", "task": "review"}}Stepping rules
- The step starts at
entry. It evaluates the function with its inputs and the answers whose key names that function. The lazy rules of section 9 apply. - A reference is followed in the same call. Only a question, a request, a final instruction, or an error ends a call. The
traceshows each hop. - A call with return (
const x: T = await REF(args);) with no answer forxwalks REF in the same call, as a hand-off. When that walk ends with text, the response is notdone. It is arequestfor the caller'sconst:keyandfunctionare the caller's, such aswork.projects, andresponseis the type of theconst.instructionis the text, a blank line, andThen answer `<key>` with the result.. Thetraceshows the hops into REF. Whenxhas an answer, REF is not walked and the caller goes on. The keys of REF's answers start with the caller's key and>, such aswork.projects>find.ready, so each call has its own answers. A second call of REF, or a recursive call, asks its questions again. A callee that awaits a function asks for its ownconstfirst. The 64-function limit still holds, so a recursion that the answers do not end is an error.rein graphandrein watchdraw functions, not calls: a function walked in several calls is marked with the answers of its first call. - A flow may visit at most 64 functions in one call. A flow that goes further is an error:
flow did not finish in 64 steps (reference cycle?). - An answer is checked before the flow runs. An unknown key is an error, and the error names the key. A value outside the type is an error, and the message lists the accepted values.
- A string parameter cannot be answered. It is an input: give it in
inputs. - A
stringparameter of the entry function that has no key ininputsis an error:missing input `pr`; give it with --input pr=<value>. - A key in
inputsthat is not astringparameter of the entry function is an error:unknown input `x`. - A project with checker errors is refused. The error lists the diagnostics. Warnings do not block a step.
- A template cannot be the entry.
Statelessness
The response depends only on the project and the request. The runtime keeps nothing between calls, so the caller sends every answer so far. A harness adds the key from the last response to answers, then steps again. The same project and the same request always give a byte-equal response. The code uses no clock, no randomness, and no global state.
CLI
text
rein step --root examples/rein/support-ticket --answer main.vip=no --json
rein step examples/rein/support-ticket/main.rein --answer main.vip=no --json
rein step --root examples/rein/support-ticket --stdin < request.json
rein step --flow support-ticket --jsonThe program is a .rein path or a rein list name, and it may be given as the positional argument. A path is the program itself: its folder is the root and its name is the entry (section 1a). --flow and --root still work.
--stdin reads one request as JSON, and then the output is always JSON. Do not combine --stdin with --entry, --answer or --input. --answer repeats for each key. An answer key may be given once. --input repeats for each name.
12. Agent integration
rein hook is the runtime side of an agent hook. The hook is built on polyhook, so the same hook code works across agents. Rein does not write the agent's hook config. There is no installer for it.
- The hook runs
rein hook --caller claude-codeon two events:SessionStartandUserPromptSubmit. SessionStartmarks the session as pending. It writes nothing to the agent.- The first
UserPromptSubmitof a pending session gets the agent context (rein agent-context) asadditionalContext. Later prompts of that session get nothing. ASessionStartfrom a resume, clear or compact makes the next prompt pending again. - Every other event is a no-op: no output, exit 0. Errors are a no-op too, with the reason on stderr. The hook never blocks the agent.
- Other tools have no hook yet.
The context block says what flows are, lists the visible flows with their descriptions, and shows the stepping rule from section 11.