Language Reference
Lashlang is a compact CodeAct language for model-authored REPL cells. Syntax is brace-delimited (expressions inside { ... }, no significant whitespace), with familiar token-level cues for agents: if / else / for x in xs / while cond, and / or / not, and Python-shaped raw strings like r"""...""" or r'...'. The parser accepts the full module grammar, but the linked host surface decides which abilities and language features exist: receiver operations, reusable background process work, trigger registry operations, process sleep, process signals, and optional static process-map labels are enabled explicitly by the host and hidden from prompts when disabled. Declarative trigger syntax has been removed; programs construct host-provided trigger-source values and call triggers.register, triggers.list, or triggers.cancel. Timers and recurring jobs are host-owned source policies, not Lashlang syntax. Fail-fast unwrap uses Rust-style ?, the ternary is cond ? yes : no, and finish / print are foreground effect expressions.
- Values
null, booleans, numbers, strings, lists, tuples, records, projected host descriptors, result wrappers, and immutableImagehandles. Resource handles and process handles appear only when the linked host surface exposes those abilities. Lists and records are value-shaped and copy-on-write at mutation roots. Tuples are immutable sequence values and serialize to JSON arrays at host boundaries.- Program form
- A program is a module with optional
typeandprocessdeclarations plus a foreground main block. Removed declarativetriggersyntax is rejected by the parser with a diagnostic that points agents at the trigger registry. Disabled abilities remain syntax-valid where applicable but are rejected by the linker with a source-spannedlashlang feature `...` is disabled by this hostdiagnostic before runtime calls can be made. Simple and path assignment, receiver operations,print,if/else,for,while,break,continue,cancel, andfinishare expression nodes; assignment and loops evaluate tonull. - Expressions
- Literals, variables, tuple comma expressions, builtin calls, receiver operations, named process starts,
await, field/index access, unary operators, binary operators, ternaries, list comprehensions, result unwrap with?, andType { ... }literals. - Effects
- All external work crosses
ExecutionHost. Receiver operations return wrapper records when resource operations are enabled, background process starts return handles when process support is enabled,awaitresolves handles, andprintcreates model-visible observations. Linked modules are content-addressedModuleArtifactrecords in the host/profile artifact store; durable Lashlang process input stores onlymodule_ref,process_ref,host_requirements_ref,process_name, and JSON args. Nested process starts reload the artifact by ref and verify the required host surface before execution.
Host Surface
The authored language is stable, but the available modules and optional syntax are host-provided. Each turn links against a LashlangHostEnvironment: a LashlangHostCatalog, LashlangAbilities, and LashlangLanguageFeatures. The Lashlang runtime derives callable tool operations from the current ToolCatalog, then merges plugin-provided Lashlang resources such as value constructors and trigger source metadata. The prompt renderer uses that same effective surface, so the model sees only operations, constructors, lifecycle forms, and annotations that will link.
- Tool operations
- Callable tools become receiver operations through explicit
LashlangToolBindingmetadata. A tool declared withLashlangToolBinding::new(["board"], "play")appears to Lashlang asawait board.play({ cell: 4 })?, while the host still dispatches the underlying tool name such asplay_move. Tools without an explicit lashlang binding are ordinary model tools, not Lashlang module operations. - Module authorities
- A module path resolves to a typed authority value such as
Board,Web, orAgents. Named processes receive those authorities explicitly:process notify(mail: Gmail) { await mail.send({ body: body })? finish true }, thenstart notify(mail: gmail.work). This keeps process authority visible in the process signature instead of hidden in globals. - Value constructors
- Plugins can add pure constructors with
LashlangHostCatalog::add_value_constructororadd_trigger_source_constructor. Trigger-source constructors require a validated named object payload type such ascron.Tick, while the constructed source value such ascron.Scheduleremains opaque. Constructors are ordinary expressions, not awaited effects:source = cron.Schedule({ expr: "0 8 * * *", tz: "UTC" }). - Ability gates
LashlangAbilitiesgates processes, sleep, process signals, and triggers. Disabled features are not shown to the model and fail during linking before anExecutionHostoperation can run.- Language features
LashlangLanguageFeaturesgates optional static syntax.label_annotationsenables@label(title: "...")and@label(title: "...", description: "...")for Lashlang execution graphs; it is disabled by default and enabled by RLM/workbench presets.
// Host/tool declaration.
ToolDefinition::raw("tool:play_move", "play_move", "...", input, output)
.with_lashlang_binding(LashlangToolBinding::new(["board"], "play"));
// Lashlang authored by the model.
board = await board.read({})?
move = await board.play({ cell: 4 })?
finish { board: board, move: move }
Program Form
The RLM protocol extracts the first complete paired block whose start tag line trims to exactly <lashlang> and whose closing tag line trims to exactly </lashlang>. Text before the start tag is visible assistant prose; only text between the tag lines is Lashlang source. Inline text such as Use <lashlang> here remains prose and does not execute.
- REPL state
- Variables persist across
<lashlang>blocks in the active RLM execution state. Read-only host bindings projected through RLM's projection API are already in scope (e.g.,history) and should be used directly instead of reconstructed from prompt prose. Lazy app projections are ref-backed and resolved before each block runs. - Completion
finishends the program and turn. When the host exposes a continuation operation,await control.continue_as({ task: ..., seed: { ... } })?is a receiver operation (not a keyword) that ends the current trajectory and activates a fresh-window AgentFrame in the same session. In schema-constrained runs, a bad final shape produces a runtime nudge asking the model to fix the value and finish again from another<lashlang>block.
The examples below assume the host has linked a lowercase workspace.default resource with the shown operations enabled.
<lashlang>
text = await workspace.default.read_file({ path: "Cargo.toml" })?
print { chars: len(text), head: slice(text, 0, 1200) }
</lashlang>
A typical AgentFrame switch packs the concrete state the AgentFrame needs and ends the cell. Computed expressions default to global state on the AgentFrame; values rooted at a projected binding (like input.prompt) stay projected, preserving their ProjectionRef when one exists.
<lashlang>
findings = []
for path in await workspace.default.glob({ pattern: "src/**/*.rs" })? {
text = await workspace.default.read_file({ path: path })?
if contains(text, "TODO") {
findings = push(findings, { path: path, chars: len(text) })
}
}
await control.continue_as({
task: "summarize the TODO audit and propose a follow-up plan",
seed: { problem: input.prompt, findings: findings }
})?
</lashlang>
Label Annotations
@label is optional static metadata for Lashlang execution graph visualization. It does not change execution semantics, resource dispatch, trigger routing, or process registry state. When label_annotations is disabled, annotated modules fail during linking with a clear disabled-feature diagnostic.
- Syntax
@label(title: "Create inbox card")or@label(title: "Create inbox card", description: "Stores the button message").titleis required,descriptionis optional, and both values must be string literals. Variables, interpolation, icons, colors, layout hints, and unknown keys are rejected.- Targets
- At top level and inside process bodies, labels can annotate ordinary statements, awaited host/resource operations, assigned operation calls,
start,sleep,wait_signal,signal_run,wake,yield,finish,fail, loops, andifbranch nodes. Before aprocessdeclaration, the label still describes the process definition. Labels on declarations other thanprocessand stacked labels are rejected. - Storage
- Label metadata is part of the
ModuleArtifactidentity. Changing a label changes the module ref. Hosts see it through Lashlang execution graphs as execution-map nodelabel_metadata; lifecycle node events continue to reference stablenode_idvalues.
For the static-map versus dynamic-update model, including an example rendered graph, see Lashlang graph updates.
process on_button(inbox: Inbox, event: ui.button.Pressed) {
@label(title: "Create inbox card", description: "Stores the button message")
result = await inbox.cards.create({ title: event.button })?
finish result
}
Syntax And Values
The language accepts newline-separated expressions. Outside comma-delimited syntactic lists, commas construct immutable tuples. Inside calls, lists, records, type fields, and process argument lists, commas keep their separator role.
- Literals
- Use
null,true,false, numbers, strings, lists like[a, b], tuples like(a, b),(a,), and(), and records like{ name: "lash", "with space": 1 }. Record insertion order is preserved for display and serialization. - Strings
"..."and'...'support\n,\r,\t, escaped matching quotes, and\\."""..."""and'''...'''are multiline with the same escapes. Raw strings such asr"...",r'...',r"""...""", andr'''...'''preserve content exactly for patches, JSON, scripts, Markdown, and heredocs.- Fields and indexes
value.fieldreads record and image fields.value[index]reads lists, tuples, and records; record indexes are string-coerced and missing keys read asnull. List and tuple indexes support negative offsets for reads and slices. Tuple index assignment is rejected because tuples are immutable.- Images
Imagevalues expose.id,.label,.size,.width, and.height. Image fields are immutable.len(image)is invalid; useimage.size.
count = grep_results.count
files = grep_results.files_with_matches
print count, files
pair = count, files
finish { pair: pair, first: pair[0] }
Expressions
Expressions are deliberately small but cover the operations agents need for filtering, shaping, validation, and tool orchestration.
| Form | Meaning |
|---|---|
a, b, (a,), () | Tuple construction. (a) remains grouping; foo(a, b) remains two arguments; foo((a, b)) passes one tuple argument. |
a + b, a - b, a * b, a / b, a % b | Numeric arithmetic. + also concatenates strings, lists, and tuples where supported by runtime value semantics. Mixed list/tuple concatenation is rejected. |
==, !=, <, <=, >, >= | Comparison operators used in conditionals and filters. |
a and b, a or b, !a, not a | Boolean composition and negation. |
cond ? yes : no | Expression ternary. Lashlang has no expression-form if. |
name(args...) | Builtin function call. User-defined functions are not part of the language surface. |
[expr for x in xs if cond] | List comprehension. Ordered for/if clauses build a new list; multiple for clauses nest left-to-right and each if filters the bindings to its left. |
await RESOURCE.alias.operation({ ... })?, (await handle)? | Fail-fast unwrap of a result wrapper: return .value on success or abort the block with the wrapper error. |
results = await (
workspace.default.read_file({ path: "left.txt" })?,
workspace.default.read_file({ path: "right.txt" })?
)
finish { left: results[0], both: results }
Assignment And Mutation
Assignments are rooted at named variables. Path assignment mutates the variable's current value through copy-on-write root updates, not through shared-alias mutation. There is no aliasing of inner records or lists.
- Supported targets
name = expr,record.field = value,record[key] = value,list[i] = value, and nested forms likestate.groups[g].count = count + 1.- Rules
- Record field/index assignment inserts or replaces fields. List assignment replaces an existing integer index only. Tuple indexes are read-only. Assigning through a missing nested record key or image field is an error.
counts = {}
for group in groups {
current = counts[group]
counts[group] = (current == null ? 0 : current) + 1
}
finish counts
Control Flow
Control flow is expression-backed but source-oriented. Loop cleanup is part of the runtime path, so loop escapes are explicit forms rather than arbitrary jumps.
if/else- Conditionals use blocks.
else ifparses as anelseblock containing anotherif. forfor item in iterable { ... }iterates lists and tuples. Non-sequence iteration is a runtime error.rangeis the usual integer-loop helper.whilewhile condition { ... }repeats while the condition is truthy. Prefer bounded loops where possible;break,continue, andfinishkeep their normal meanings inside the loop body.break/continuebreakexits the nearest loop through the iterator cleanup path.continueskips to the next iteration. They are rejected outside loops.finishfinish valueends the whole program or turn.finishinside a loop is not a loop control form.
A short walkthrough that exercises iteration, else if chains, break/continue, and format. Loop variables are scoped to the loop body and the prior binding of label is restored on break.
nums = [1, 2, 3, 4, 5, 6]
seen = []
total = 0
for n in nums {
if n == 2 {
continue
}
if n > 4 {
break
}
seen = push(seen, n)
total = total + n
}
if total > 10 {
label = "large"
} else if total > 5 {
label = "medium"
} else {
label = "small"
}
finish format("seen={} total={} label={}", join(seen, ","), total, label)
while is useful for bounded retry/fill loops when the limit is computed inside the program.
attempts = 0
items = []
while len(items) < 3 and attempts < 5 {
attempts = attempts + 1
candidate = format("item-{}", attempts)
if contains(items, candidate) {
continue
}
items = push(items, candidate)
}
finish { attempts: attempts, items: items }