Library
Standard Library
The standard library is a set of thin pure fn wrappers written in Luma under luma/std/. All of them are deterministic; they can be called freely inside where rules, @valid expressions and agent methods. The one exception is std/io — it consists of io fns and cannot be used inside where/@valid.
Usage and the alias rule
import math from "std/math" // math.abs(...) import "std/math" // abs(...) — no namespace
In an agent method body, a line of the form x = name.function(...) is ambiguous: it could be a standard-library call or a call on a uses dependency. The parser resolves it against a set of namespace names, which is the ten conventional aliases below plus whatever alias you actually imported under. Anything else is treated as a dependency call.
Three consequences, each measured:
- You may import under any alias.
import numbers from "std/numeric"followed bynumbers.round(3.7)works — your alias is registered when the import is resolved. The table below is convention, not a constraint. - A
usesdependency may not be named after one of the ten.uses col: Helperthenr = col.give()parses as a namespace call, never reaches the dependency, and fails at runtime with'col' variable is not defined yet.— a message that says nothing about dependencies. - A name that is neither — a typo, or an import you forgot — fails by name at parse time:
'helper' is not defined as a dependency.
| Module | Conventional alias | Kind |
|---|---|---|
std/math | math | pure |
std/string | str | pure |
std/date | date | pure |
std/collections | col | pure |
std/regex | regex | pure |
std/numeric | num | pure |
std/json | json | pure |
std/schema | schema | pure |
std/agent | agent | pure |
std/findings | findings | io |
std/io | io | io |
agent A {
uses helper: Helper
fn f() -> S {
r = helper.give()
return { v: r.v }
}
}agent A {
uses col: Helper
fn f() -> S {
r = col.give()
// Error: 'col' variable is
// not defined yet.
}
}std/math
This module is written entirely in Luma — you can read its source in luma/std/math.luma.
Absolute value.
math.abs(0 - 7) // 7
The sum of the numbers in the list. 0 for an empty list.
math.sum([1, 2, 3]) // 6
The smaller of two numbers.
The larger of two numbers.
Clamps the value into the given range.
math.clamp(150, 0, 100) // 100
import math from "std/math"
schema Calculation {
values: float[1..10]
total: float
where math.abs(math.sum(values) - total) <= 0.01
}std/string
Converts to lowercase.
Converts to uppercase.
Strips only leading and trailing whitespace. Interior whitespace is preserved.
str.trim(" Ada Byron ") // "Ada Byron"Prefix check.
Suffix check.
Replaces every match. It performs a plain-text search — this is not a regex.
str.replace("a-b-c", "-", "/") // "a/b/c"Length in UTF-16 units — an astral character counts as 2. (Does the same job as the built-in length.)
import str from "std/string"
schema Text {
name: string @valid(x => str.startsWith(str.trim(x), "Ada"))
slug: string @valid(x => str.lower(x) == "luma-dsl")
description: string @valid(x => str.textLength(x) > 5)
}str.trim does not touch repeated whitespace inside the value. Use a regex to normalize it:
import regex from "std/regex" normalized = regex.replace(raw, "\\s+", " ")
std/date
Accepted formats: YYYY-MM-DD and a full ISO 8601 date-time (a Z or ±HH:MM offset is required). Calendar-invalid dates (2024-02-31) are rejected. Every field read is performed in UTC.
Is the value parseable and a real date?
left < right. If either side is invalid it returns false instead of throwing.
left > right. false on invalid input.
The whole-day difference between the two (the fractional part is discarded). On an invalid date it throws.
date.diffDays("2024-01-01", "2024-01-06") // 5The UTC year. Throws on an invalid date.
The UTC month, 1–12 (not zero-based). Throws on an invalid date.
The UTC day of the month. Throws on an invalid date.
Returns the first 10 characters of the value — reduces a date-time to YYYY-MM-DD.
date.toDate("2024-01-02T10:20:30Z") // "2024-01-02"import date from "std/date"
schema Period {
start: string @fmt("date") @valid(x => date.isValid(x))
end: string @fmt("date")
days: int @valid(x => x > 0)
where date.isBefore(start, end)
where date.diffDays(start, end) == days
where date.year(start) >= 2024
}The comparison functions (isBefore/isAfter) silently return false on invalid input; the calculation functions (diffDays, year, month, day, toDate) throw. On nullable date fields, guard with date.isValid first.
std/collections
Are all elements true? true for an empty list.
Is at least one true? false for an empty list.
Does the list contain the element?
Joins a list of strings.
Drops duplicates; order is preserved.
Flattens nested lists completely.
col.flatten([["a"], ["b", "c"]]) // ["a", "b", "c"]
import col from "std/collections"
schema Collection {
tags: string[] @valid(x => col.contains(x, "vip"))
groups: string[][]
flags: bool[] @valid(x => col.any(x))
where col.contains(tags, "vip")
where contains(col.flatten(groups), "approved")
where col.join(tags, ",") == "vip,active"
}The built-in contains searches for a substring in a string as well as an element in an array, and it needs no import. col.contains, on the other hand, is typed for string lists only. When you are searching inside a scalar string, use the built-in one:
name: string @valid(x => contains(x, "Ada")) // built-in tags: string[] @valid(x => col.contains(x, "vip")) // std/collections
Building and ordering (v0.4.0)
Returns a new list with the value at the end. No std function mutates its input.
Concatenates two lists into a new one.
Ascending, stable sort. Comparable element kinds are string, int and float, and the two ways a list can be unsortable refuse with different messages: an element of another kind gives col.sort supports string, int and float values., while comparable elements of mixed kinds give col.sort values have mixed types. A non-list argument gives col.sort expects a list.
Sorts a list of records by a field, ascending and stable. The key is a string field name, not a lambda — Luma has no lambda values. The field must be present on every item and single-typed.
by_amount = col.sortBy(items, "amount")
Same contract as col.sortBy, descending.
The keys of a map, in its deterministic iteration order.
Slicing and shaping (v0.4.0)
Elements from start up to (excluding) end. Negative bounds refuse by name; start > end gives an empty list; end beyond the length is clamped.
First count elements. count beyond the length returns the whole list — not an error. Negative counts refuse by name.
Everything after the first count elements; beyond the length gives an empty list.
Splits into buckets of size; the last bucket may be shorter. size < 1 refuses by name.
col.chunk([1, 2, 3, 4, 5], 2) // [[1, 2], [3, 4], [5]]
Pairs elements by index into two-element lists; the longer list is cut to the shorter one.
A new list in reverse order.
Index of the first match, or -1.
First element. An empty list refuses by name: col.first on an empty list.
Last element; same empty-list contract as col.first.
Grouping and aggregation (v0.4.0)
Buckets records by a string-valued field. Key order is deterministic (sorted by the field's value); a non-string key field refuses by name.
buckets = col.groupBy(orders, "region") // map<Order[]>
First record per distinct field value. The result is ordered by field value, not by input order — it delegates to col.sortBy; only which record wins inside a bucket follows input order.
Sum of a numeric list; 0 for an empty list. Accepts int lists.
How many records carry value in field; 0 when none match.
The Luma-written collection functions express their edge contracts through parameter annotations (@valid + @feedback), so their errors carry the validation prefix: Error: pure col.first.values: col.first on an empty list. The message text after the prefix is pinned by tests, byte for byte.
std/regex
Patterns use JavaScript regex syntax. Pattern length is capped at 1000 characters; an invalid pattern throws.
Does the pattern occur in the text? (test semantics — write ^…$ for a full match.)
Replaces every match (global flag).
regex.replace("Ada 42 apples", "[0-9]+", "#") // "Ada # apples"Returns the first match; null if there is none.
The number of matches; 0 if there are none.
Does the pattern compile? Does not throw.
import regex from "std/regex"
schema RegexExample {
code: string @valid(x => regex.matches(x, "^[A-Z]{3}-[0-9]+$"))
text: string
found: string | null
where regex.count(text, "a") == 2
where regex.find(text, "[0-9]+") == found
}The && and || combinations in a @valid body are split with string and parenthesis awareness; a | character inside a regex pattern does not break the expression:
code: string @valid(x => regex.matches(x, "^(AA||BB)-[0-9]+$"))
std/numeric
Rounds to the nearest integer. Throws on a non-finite value.
Rounds down.
Rounds up.
Is it a finite number? false for NaN/Infinity.
Is it an integer?
|left − right| ≤ tolerance. For floating-point comparisons this is the right tool.
num.approximatelyEqual(0.1 + 0.2, 0.3, 0.0001) // true
Clamps the value into the range.
import num from "std/numeric"
schema InvoiceItem {
quantity: float | null
unit_price: float | null
total: float | null
where quantity == null || unit_price == null || total == null || num.approximatelyEqual(quantity * unit_price, total, 0.01)
@feedback("does not match quantity * unit_price")
}Direct equalities such as where total == subtotal + tax fail because of floating-point error even with solid models, and cause needless retries. num.approximatelyEqual(…, 0.01) is a suitable choice for cent-level precision.
std/json
Converts JSON text into a value. If it is invalid, it throws Invalid JSON text.
Compact JSON text.
JSON text indented with 2 spaces.
Identical to stringify — a separate name for readability.
Is it parseable? Does not throw.
Is there a value at the dot-separated path? true when it is not undefined.
json.has(data, "seller.name")
Returns the value at the path if it is a string; null when it is missing or of a different type.
Returns the value at the path if it is a number, otherwise null.
Returns the value at the path if it is a boolean, otherwise null.
A value from json.parse has type any, and on any a direct field access throws when the field is absent:
Error: Field 'd.isyeri' was not found.
So you cannot guard it the obvious way — if d.isyeri != null throws on the access itself, before the comparison happens. That is the same rule as xs[5] is out of bounds (length 2). and m["missing"] is not present., and it is deliberate: a silent null here would flow onward and end up inside validated output, which is the one thing this language exists to prevent. It also means a typo in a field name is caught rather than read as "absent".
When the shape is under your control, declare a schema and let validation do the work. When it is not — heterogeneous payloads, an ext.json column, a third-party webhook — json.has plus the json.get* family is the navigation surface, and the get* functions return null for missing and for wrong-typed, so one check covers both.
import json from "std/json"
schema JsonExample {
raw: string
data: any
payload: json<Payload>
where json.isValid(raw)
where json.has(data, "id")
where json.getString(data, "id") == "p1"
where json.getNumber(data, "count") == 2
}
agent JsonProducer {
fn create() -> JsonExample {
raw: string = "{\"id\":\"p1\",\"count\":2}"
data: any = json.parse(raw)
payload: Payload = json.parse(raw)
return { raw: raw, data: data, payload: payload }
}
}json.getString(v, "a.b") returns null both when "the path does not exist" and when "the value is not a string". If the distinction matters, check with json.has(v, "a.b") first.
std/schema
Schemas are compile-time definitions and cannot be modified at runtime; this module only reads them. When a schema name appears inside an expression it automatically becomes a schema reference.
The schema name.
The schema-level @desc text.
The schema-level @prompt text. It is not sent to the model automatically; you add it to the prompt yourself.
The full descriptor: { name, description, prompt, fields, rules, components }.
The list of field descriptors (shape below).
The descriptor of a single field; null if it does not exist.
Does the dot-separated path exist? It descends into nested schemas and array element types.
schema.hasField(InvoiceExtract, "seller.name") // true
The list of where and conditional rules: { kind: "where" | "conditional", expression, feedback }.
The composition parts. On a tagged union it returns the member names instead — official since v0.9.0 and locked by a test. [] when the schema is neither. There is no name → reference bridge: the strings name members, they do not resolve to them.
The descriptor list for several schemas. A repeated schema is an error.
The JSON Schema counterpart of a single schema. It does not include $defs.
A name → JSON Schema mapping.
Shape of a field descriptor
{
"name": "name",
"type": "string",
"optional": false,
"description": null,
"title": "Full name",
"format": null,
"aliases": [],
"deprecated": null,
"examples": ["Ada Byron"],
"constraints": { "minLength": 2, "maxLength": 40 }
}The type field is written in Luma syntax: "string", "int", "LineItem[]", "map<float>", "string | null", "enum[\"a\", \"b\"]". optional becomes true when the field is marked with ? or carries a @default. constraints contains only the constraints that are actually defined.
import schema from "std/schema"
import json from "std/json"
agent SchemaInspector {
fn inspect() -> ReflectionResult {
catalog = schema.catalog([Invoice, Receipt])
field = schema.field(Invoice, "invoice_number")
json_schema = schema.toJsonSchema(Invoice)
return { name: schema.name(Invoice)
, description: schema.description(Invoice)
, field_name: field.name
, has_field: schema.hasField(Invoice, "invoice_number")
, catalog_count: length(catalog)
, rule_count: length(schema.rules(Invoice))
, json_description: json.getString(json_schema, "description") }
}
}Writing the extraction instruction inside @prompt and reading it back with schema.prompt(...) makes the instruction versioned together with the schema — when you add a field you update the instruction in the same place.
instruction = schema.prompt(InvoiceExtract)
result = ask "{instruction}\n\nMARKDOWN:\n{markdown}" -> InvoiceExtractstd/agent
Deterministic helpers for reading error objects and assembling prompt fragments. This module does not wrap ask, tool, retry or memory — those are features of the language itself.
Reads the error code (TOOL_TIMEOUT, MODEL_RATE_LIMIT…). null when there is no code.
Reads the error message.
The retryable flag; false when absent.
The structured details object; null when absent.
Produces a typed tool-error object.
agent.toolError("VALIDATION_FAILED", "Invoice could not be validated", true, { field: "total" })Produces a section in the form "Title:\ntext".
Joins the sections with a double newline.
import agent from "std/agent"
agent Helper {
tools: [db_write]
fn run(data: string) -> Result {
try {
written = tool db_write({ request_id: "r-1", value: data })
return { status: "ok", message: "" }
} catch error {
if agent.isRetryable(error) {
return { status: "temporary_error", message: agent.errorMessage(error) }
}
return { status: agent.errorCode(error), message: agent.errorMessage(error) }
}
}
}Building a prompt
prompt = agent.compose([
agent.section("Instruction", "Return valid JSON only."),
agent.section("Rules", "Set any field you cannot find to null."),
agent.section("Data", json.pretty(record))
])
result = ask "{prompt}" -> OutputInstruction:
Return valid JSON only.
Rules:
Set any field you cannot find to null.
Data:
{ … }std/findings (v0.8.0)
Reads the flags a @severity("warn") rule left on a value. A soft rule does not stop the run; what it found is recorded against the operation that produced the value, and this module is how the program gets at it.
The findings recorded for this value; an empty list when there are none. Reading marks them read, which is what silences the end-of-run warning.
How many there are, without materialising the list. Also counts as reading.
Field of Finding | Type | What it holds |
|---|---|---|
rule | string | The rule that failed, as valid:<expression> or where:<expression>. The ceiling's marker uses findings.capped. |
path | string | Where it failed, e.g. Maker.make.return.score. |
message | string | The text the rule would have failed with. |
severity | string | "warn" today. |
attempt | int | The attempt the value was accepted on; 1 when there was no retry. |
import findings from "std/findings"
schema Scored {
score: int @valid(x => x >= 0) @severity("warn")
}
agent Auditor {
uses maker: Maker
fn audit() -> Audit {
shaded = maker.make(-7)
flaws = findings.of(shaded)
return { score: shaded.score, flaws: length(flaws), first_rule: flaws[0].rule }
}
}Both functions read run state, so they are io — @valid, where and pure fn bodies refuse them by name:
Error: findings is io and cannot be used inside @valid or where rules: findings.count
That refusal is the point: without it a rule could read its own violations, and its result would depend on the order the rules ran in.
Findings live per operation, and per element under parallel over. A value that was never flagged simply returns an empty list — there is nothing to guard against before calling. The full semantics, including the ceiling and the unread warning, are on the Schemas & Validation page.
std/http (v0.4.0)
std/http talks to HTTP services with zero dependencies — it rides on the runtime's built-in fetch. It pays for that with a subprocess per call, roughly 30–80 ms: the expression evaluator is synchronous and there is no synchronous fetch, so the request runs in a child process. That is fine for coarse-grained calls — paginated pulls, a few hundred requests — and the wrong tool for enriching every row of a large set, where the process cost dominates. Tracked in ROADMAP.md § U5. Every function returns the standard library's first exported schema:
schema HttpResponse {
status: int
body: string
headers: map<string>
}GET. timeout is milliseconds and mandatory — there is no default; a silent infinite wait is a silent failure.
POST with a JSON body; content-type defaults to application/json.
PUT with a JSON body.
PATCH with a JSON body.
DELETE.
A 404 or a 500 returns normally — deciding what a status means is the program's job. The only failure that throws is not reaching the host at all: http.get could not reach '<url>': <reason>.
import http from "std/http"
response = http.get("https://api.example/v1/orders", { "authorization": token }, 5000)
if response.status == 200 {
data = json.parse(response.body)
}Redaction goes by the header's name, not by how sensitive its value is, so do not rely on it as a general secret filter. A header whose name matches api_key · apikey · api-key · password · secret · credential · authorization (case-insensitive, substring) has its whole value replaced with [REDACTED] — x-api-key is covered by that rule, authorization loses the entire value rather than just the bearer token. Separately, any secret value the runtime already knows (the api keys resolved from the environment and from model profiles) is replaced wherever it appears. Every other header goes to the trace in plaintext — cookie, x-auth-token, x-session, a signature header. The URL and the body are never logged at all. Response bodies are capped by --max-response-bytes (default 10 MB) — exceeding the cap refuses by name instead of silently ballooning. Retries are not http's job: use the agent retry / program try machinery.
std/state (v0.4.0)
std/state is the incremental-run primitive: a flat key→string store backed by one JSON file. A watermark is just a stored string — no separate abstraction.
The stored value, or null when the key has never been written — a first run can tell itself apart.
Persists and returns the value. The write is atomic (temp file + rename): an interrupted run cannot corrupt the store.
All stored keys.
import state from "std/state"
cursor = state.read("orders_watermark") // first run: null
state.write("orders_watermark", latest)The store's path comes from --state-file <path> and there is no default: without the flag, state.read/state.write refuse by name (state.read needs a state file; pass --state-file <path>.). A silent default location would be silent data loss. State is a real file and is not part of --record/--replay.
Values are always strings; a program that stores numbers or dates converts explicitly with json/date. --state-file is required whenever any state.* function is called, state.keys() included — there is no default location, because a silent one is silent data loss the first time two jobs share a machine.
std/io
std/io functions cannot be used inside where, @valid or pure fn. They can only be called in agent methods and inside io fns. Every path is confined to the project root. For in-depth usage → IO & Multimodal.
io.readFiles is bounded by --max-rows (default 200000), which is a global ceiling on row-producing paths rather than an io quirk — the same limit and the same message govern a query result. Exceeding it refuses by name and points at col.chunk: Result set exceeded the row limit (200000). Process in batches with col.chunk, or raise --max-rows. See Flags and ceilings.
Reads a text file as UTF-8.
Writes a text file, creating the intermediate directories. Returns the root-relative path of the file it wrote.
Reads a binary file: { path, mime_type, base64, size }.
Reads several files in the same shape. Capped by --max-rows — see the note below.
A naturally sorted list of @-prefixed paths. The pattern is a glob over the file name only (*, ?); it cannot contain /.
pages = io.listFiles("@0213", "*.png", false)Does the file/directory exist?
The MIME type derived from the extension; application/octet-stream when unknown.
Converts an io.readFile result into a data:<mime>;base64,… URL.
Converts a file list into line-by-line {{image_url:…}} / {{file_url:…}} markers.
import io from "std/io"
agent InvoiceReader {
model: Vision
fn transcribe(folder: string) -> Transcript {
pages = io.listFiles("@" + folder, "*.png", false)
files = io.readFiles(pages)
markers = io.toMultimodalMarkers(files)
text = ask "Convert every page to Markdown: {markers}" -> Transcript
written = io.writeText("@" + folder + "/pre.md", text.markdown)
return text
}
}