Luma public docs
Türkçe

Operations

Error Reference

Luma normalizes its model and tool errors: each of those carries a code, a message and a retryable flag, and that flag directly determines how the retry loop and the run policies behave. Parse errors, validation errors and the entire v0.4.0 data surface stay outside that normalization — they carry a message and nothing else.

Error shape

Error: [TOOL_INPUT_INVALID] calculate.input.a must be a number.

CLI errors have the form Error: [CODE] message and terminate with exit code 1. The code appears only on normalized model/tool errors. Parse errors, validation errors and every data surface error — datasource/query/command, std/http, std/state, parallel over — carry no code, so their CLI line starts straight at the message with no [CODE] in front of it.

When it is caught inside try/catch, the error is an object:

json
{
  "code":      "TOOL_TIMEOUT",
  "message":   "Tool 'http_get' timeout occurred (3000 ms).",
  "retryable": true,
  "details":   { "url": "https://…" }
}
luma
import agent from "std/agent"

} catch error {
  code = agent.errorCode(error)        // "TOOL_TIMEOUT"
  msg  = agent.errorMessage(error)
  if agent.isRetryable(error) {
    ...
  }
}

Errors that were never normalized carry only message: a type error inside a pure fn, and every data surface error as well. For those agent.errorCode returns null and agent.isRetryable returns false — the flag reads the error's own retryable field, and an uncoded error has none. Match them on the message text.

Model errors

CodeStatusWhenWhat to do
MODEL_PROVIDER_ERRORpermanent / retryableThe gateway answered with something other than 2xx (429 and 408 aside).5xx, 409 and 425 are retryable. For the permanent ones, check base_url, the model identifier and your authorization.
MODEL_RATE_LIMITretryableHTTP 429.Switch to a second model with a fallback policy, or slow the request rate down.
MODEL_TIMEOUTretryableHTTP 408, or a client-side timeout.Raise the timeout value; split large prompts.
MODEL_CANCELLEDpermanentHTTP 499 or a cancellation signal (the loser of a race, a parallel timeout).Usually the expected behavior.
MODEL_EMPTY_CONTENTretryableThe provider returned no content.max_tokens may be too low; a streaming response may have been cut off early.
MODEL_INVALID_RESPONSEretryableThe response could not be parsed, was not valid JSON or did not pass schema validation.The most frequent error. Give it retry, narrow the schema, add @desc and @feedback.
MODEL_REFUSALpermanentThe model explicitly refused the request.Rewrite the prompt — it does not retry, and it should not.
MODEL_INVALID_TOOL_CALLretryableThe tool call arguments were not valid JSON.Simplify the tool's parameter schema.
MODEL_UNSUPPORTEDpermanentAn unrecognized provider value. Normally caught at parse time; this code exists only as a defensive fallback.Check the valid provider names here.
MODEL_BUDGET_EXCEEDEDpermanenttoken_budget, cost_budget or fusion_input_tokens was exceeded.Raise the budget, or reduce the prompt/memory size.
MODEL_VOTE_NO_QUORUMretryableNo quorum formed under a vote policy.Lower quorum or narrow the schema — free-text fields make unanimity impossible.
MODEL_VOTE_TIEpermanentQuorum was reached but there is a tie, and tie: "fail".Use tie: "first" or "highest_confidence".
MODEL_VOTE_CONFIDENCE_UNAVAILABLEpermanenttie: "highest_confidence" is selected but the results carry no numeric confidence.Add confidence: float [0..1] to the schema.
MODEL_FUSION_NO_INPUTSretryableNo candidate succeeded under a fusion policy.Check the candidate models and their schema compatibility.

HTTP status mapping

StatusCodeRetryable
408MODEL_TIMEOUTretryable
409 · 425MODEL_PROVIDER_ERRORretryable
429MODEL_RATE_LIMITretryable
499MODEL_CANCELLEDpermanent
5xxMODEL_PROVIDER_ERRORretryable
other 4xxMODEL_PROVIDER_ERRORpermanent

The error.code and error.type from the provider's body, together with the first 1000 characters of the raw response, are redacted and then added to the details field.

Tool errors

CodeStatusWhenWhat to do
TOOL_NOT_FOUNDpermanentThe contract exists but there is no handler in the registry.Add the handler to src/tool-registry.ts, or load it with --tool-module.
TOOL_INPUT_INVALIDpermanentThe input did not pass schema validation.On native calls the arguments the model produced are the faulty part — improve the parameter @descs.
TOOL_OUTPUT_INVALIDpermanentThe value the handler returned does not match the output schema.Fix the handler; add type safety with --generate-tool-types.
TOOL_TIMEOUTretryableThe effective timeout was exceeded.Raise timeout/tool_timeout; write the handler so that it responds to AbortSignal.
TOOL_CANCELLEDpermanentThe enclosing flow was cancelled (a parallel timeout, a race, Ctrl-C).Usually the expected behavior.
TOOL_PERMISSION_DENIEDpermanentThe credential is missing, the scope is not permitted, deny_write blocked it, or approval was not granted.The message tells you which one it was. Set LUMA_APPROVED_TOOLS or tool_scopes.
TOOL_CONTEXT_INVALIDpermanentThe needs context does not exist, tool_contexts does not allow it, or its type does not match.Check tool_contexts in luma.config.json and the agent's allowlist.
TOOL_EXECUTION_ERRORretryable / permanentThe handler threw an exception.Use LumaToolError for a typed error and decide retryable yourself.
TOOL_ERROR_INVALIDpermanentThe thrown error does not match the error: schema in the contract.Make the error object conform to the schema.
TOOL_IDEMPOTENCY_CONFLICTpermanentThe same idempotency key was used with different input.Review how the key is generated — it must be unique per request.
TOOL_CIRCUIT_OPENpermanenttool_failure_limit was exceeded.Fix the subsystem; raise the limit only deliberately.

Data surface errors

The v0.4.0 data surface — datasource/query/command, std/http, std/state and parallel over — raises named errors, and not one of them carries a code. They are thrown as plain errors, so in a try/catch agent.errorCode(error) returns null and agent.isRetryable(error) returns false for every message on this page. Match them by message text. The declarations themselves are documented under Data & Integration; this page owns their error view, Telemetry owns their trace view.

Error: query 'Orders' expected exactly one row, got 3.

No [CODE] in front of it — that is the shape of every message below. Some are raised at parse time, before the program starts; those are marked in the tables.

datasource · query · command

MessageCause and fix
datasource 'X' has no registered bridge; export it from a tool module as datasources.X.The declaration exists but nothing registered a bridge for it. Luma owns the contract, you own the driver: a tool module must export datasources.X. Load it with --tool-module or via tool_modules in luma.config.json.
datasource 'X' is missing credential: <ENV>.The environment variable named by credential: is not set. Only presence is checked — the value is never read into a prompt, a trace or an error message.
query 'X' bridge must return a list of rows.The bridge answered a query with something that is not an array. Return [] for an empty result, never null.
command 'X' bridge must return the affected row count as an integer.A command is declared -> int and the bridge must produce exactly that number — not the rows, not a driver result object.
query 'X' expected exactly one row, got N.-> Schema (rather than Schema[]) declares exactly one row; N is what the bridge actually returned, zero included. Declare Schema[] when the count is not guaranteed.
query|command 'X' timed out after N ms on datasource 'Y'.The datasource's timeout elapsed while the bridge was running. The message opens with the declaration kind, so the same shape covers both.
Result set exceeded the row limit (200000). Process in batches with col.chunk, or raise --max-rows.Global to every row-producing path: the same ceiling and the same message guard query results and io.readFiles alike. The number in the message is the effective limit, which --max-rows changes. See limits.
query 'X' sql cannot interpolate; use $1 parameters.Parse time. A {name} shape inside the sql string. Parameters bind positionally as $1..$n, so the injection surface is closed in the language itself. A command is rejected with the same message.
Datasource 'X' does not support 'transaction'; it is deliberately out of this package — handle transactions inside the bridge (ROADMAP § U4).Parse time. Transactions are absent on purpose: a transaction boundary belongs inside the bridge, and declaring one here would commit the language to semantics no contract can enforce. Any other unknown key gets has no key 'k'; known keys: … instead.

std/http · std/state

MessageCause and fix
http.<fn> could not reach '<url>': <reason>.The only network failure that is raised at all. reason is timed out after N ms when Luma's own timeout fired, and connection failed for everything else — DNS failure, refused connection and TLS error normalize into that one bucket because engines disagree on how to classify them. A third form appears if the request child process itself never answers (child exited with status N).
http.<fn> response exceeded the byte limit (10485760); raise --max-response-bytes if this is intended.The response body is larger than the ceiling (10 MiB by default). The body is discarded, not truncated.
http.<fn> timeout must be a positive integer of milliseconds.timeout is a mandatory argument with no default, and 0, negatives and fractions are all rejected: a silent infinite wait is exactly the failure class this package exists to remove.
http.<fn> expects a url string. · http.<fn> expects headers as a map.Argument shapes. Pass {} when there are no headers — the argument itself is not optional.
state.read needs a state file; pass --state-file <path>.Every std/state function refuses by name when --state-file is missing; the name in the message is the function you called. There is no default location, because a silent default is silent data loss.
state store could not be read: <file> (<reason>).The state file exists but is not a JSON object. A missing file is not an error — it reads as an empty store.

parallel over

MessageCause and fix
parallel over 'X' requires a limit.Parse time. The concurrency ceiling is mandatory — an unbounded fan-out over a query result is a way to take down the datasource behind it.
parallel over 'X' on_error fallback is not supported; use on_error continue or the default fail_fast.Parse time. Data-parallel work has no fallback branch to fall into; only continue and fail_fast exist here.
parallel over cannot be nested.Parse time. A nested fan-out multiplies the two limits together and the effective concurrency stops being readable from the source.
parallel over 'X' body must return a <Schema>.Parse time. The body contains no return at all.
parallel over 'X' body did not return a value for '<item>'.Runtime: a return exists but the path this element took never reached it — a return inside an if whose condition was false. Every element must produce a value; use on_error continue if some elements are meant to drop out.
parallel over 'X' expected a list from '<expression>', got <kind>.The collection expression did not resolve to a list. <kind> is the JavaScript-level kind of what arrived (object, null, nothing).
An HTTP status is not an error

http.get returns { status, body, headers } for a 404 and a 500 exactly the way it does for a 200: the status code is data, not a failure. A try/catch around an HTTP call never fires for a bad status — the only failure it raises is not reaching the host. Check response.status yourself, and treat an unchecked status as an unhandled error path.

Retry semantics

an error occurs
   ↓
retryable == false ?
   ├── yes → the loop stops immediately, the error is thrown upward
   └── no ↓
attempt count < retry ?
   ├── no → the error is thrown
   └── yes ↓
the error text is appended to the prompt → new attempt_id → request again
LayerSettingWhat it repeats
ask retryretry N, or the agent's retry:The entire LLM request; the error text is appended to the prompt.
Tool retryretries in the tool contractOnly the handler; it never touches the LLM.
Run policyfallback + on: [...]Moves on to a different model.
These three layers nest

Under a fallback policy every candidate model runs with retry: 0 — the resilience comes from the policy itself. Tool retries, on the other hand, are entirely independent of the LLM retry and use the tool's own retry_backoff timing.

retryable: false stops everything

When a tool throws a retryable: false error, it is not only the tool retry that stops but the LLM retry loop that called it as well. This is deliberate: on a permanent permission error there is no point in making the model repeat the same request.

Validation messages

Validation errors carry no code, and all the errors of a single round are combined into one report:

text
Error: A.f.v.name must be at least 2 characters.
A.f.v.age must be an integer.
A.f.v.email must be in email format.
A.f.v.tags must contain unique items.
A.f.v.score must be at most 100.
A.f.v.role must be one of these values: admin, user.
A.f.v.extra is not defined in the schema.
Message patternCause
X.y missing.A required field is absent.
X.y must be a string. · must be an integer. · must be a number. · must be a boolean.Type mismatch. must be a number is also emitted for NaN/Infinity.
X.y must be one of these values: …Enum violation.
X.y must be an array.An array was expected and another type arrived.
X.y must contain exactly N items.Tuple length.
X.y must contain at least/at most N items.@minItems / @maxItems.
X.y must contain unique items.@unique.
X.y.field values must be unique.@uniqueBy.
X.y must contain the expected item.@contains.
X.y must be at least/at most N characters.@length.
X.y does not match the expected format.@match.
X.y must be in <format> format.@fmt.
X.y must be at least/at most N.@range, or the range shorthand.
X.y does not satisfy its @valid rule: <expression>@valid.
X.k is not defined in the schema.Unknown field.
X.y was given more than one alias: a, bThe canonical field is absent and two or more of its @alias names arrived at once — the runtime will not pick a winner.
X does not match any union type: … | …No member of the union held.
X does not satisfy its where rule: <expression>A where invariant. A where that returns a non-boolean fails separately with X where rule must return a boolean result: ….
X conditional rule 'a' requires/forbids field: <condition>A when rule; 'a' is the field name the branch demands or forbids.
X.sources.sources contains an invalid context source: <id>An undefined context id. sources is the runtime's built-in source-validation field; the name is hardcoded and cannot be renamed — and it appears twice in the message, because the path already ends in the field name.
X.y cannot be given by its canonical name and an alias at the same time.Both the canonical name and the alias were sent.

If @feedback is defined, these messages are replaced with Path.field: <your message>, where your own text takes the place of the standard one.

When the rule is soft, the same text arrives elsewhere

A rule carrying @severity("warn") produces the message above, but it does not produce an error on a result path. The text travels in the finding's message field instead, and the run continues. Two lines are specific to that path and are not errors at all:

LineWhere it appearsWhat it means
Warning: 2 findings accepted but never read: A.f.return (1), B.g.return (1). Read them with findings.of(...) from std/findings.Standard error, at the end of the runFindings were produced and nobody read them. The exit code stays 0 — see @severity.
Findings cap reached (1000); 412 more soft violations were not recorded.The message of a finding whose rule is findings.cappedAn operation went past the per-operation ceiling. The count of what was cut is always in the record.

On an input path — a tool argument, a method or pure fn parameter — @severity is ignored and the ordinary error above is what you get.

Parse and startup errors

These occur before the program starts running, and they carry no code.

MessageCause
Invalid schema field: …The field syntax is wrong — a stray token, or a keyword continuation line such as breaking before require.
A schema line may define only one field: …Two name: type pairs on the same line. A trailing comma after a single field is fine.
Unknown annotation: @xAn unrecognized annotation. @prompt cannot be used on a field — only in the schema header.
@x can only be used once on a field.A single-use annotation was repeated.
@length can only be used on string fields…The annotation does not match the field's type.
Field range cannot be specified with both legacy syntax and @range.float [0..1] was written together with @range.
No schema named 'X' was found.An undefined schema reference.
No model named 'X' was found.An undefined model profile in an agent.
'X' is not registered in the tool registry.The contract exists, the handler does not.
Agent method f cannot use ask and return together.A terminal ask together with a return.
fn f return schema differs from the ask schema.The method's output does not line up with the ask target.
Agent 'A' does not allow tool 'x' to be called directly.The tool is not in the agent's tools list.
'x' is not defined as a dependency.An x = name.f(...) through a name that was never declared with uses. It also shows up with std module aliases → reserved namespaces.
'dependency' cannot be used inside a pure fn.The same cause, this time in a pure fn body.
IO cannot be used inside pure fn 'f'.A std/io call inside a pure fn. Use an io fn.
where is deterministic and cannot use IO.It is where that must stay deterministic, so any io.* call inside it is rejected.
ask, agent call or parallel not found.The program has no top-level operation.
.env must define base_url, api_key, and model.A legacy gateway is required but has not been configured.
Model 'X' is missing credential: api_key.The environment variable requested through getEnv does not exist.
Import module may contain only schemas and exported pure fns: …The imported file contains an agent/tool/operation.
Import cycle found: … · Agent dependency cycle found: … · Pure fn cycle found: …A circular dependency.
io path cannot escape the project directory: …The IO path is outside the root.
Model 'X' has unknown provider 'y'. Valid values: …An unrecognized provider value; the error is raised at parse time and it lists the valid values.
@valid expression is not supported: …An unsupported @valid lambda body; it is rejected at parse time whether it sits on a schema field, a tool parameter or an fn parameter.
@severity value must be "error" or "warn": reviewAn unknown severity. The set is closed at two — a tier vocabulary of your own belongs in the code that reads the findings.
@severity must follow the @valid rule it modifies.@severity with no @valid in front of it. On a field it modifies the rule it follows, never the field as a whole.
where only supports the @feedback and @severity annotations: @descAny other annotation on a where line.
findings is io and cannot be used inside @valid or where rules: findings.countstd/findings reached from a rule that must stay deterministic. It stops a rule from reading its own violations.
pure fn X parameter block not closed.An unclosed parenthesis in the signature.
Operator '%' is not supported: y % 2An operator the language does not have. The message names the operator and quotes the line, so it cannot be mistaken for the undefined-variable error below. Applies to ?. ** % & | and ? :.
'??' cannot be mixed with '&&' or '||' without parentheses: n ?? b || bThe reading would be ambiguous; parenthesise the side you meant. Both (n ?? b) || b and b || (n ?? b) are legal.

Diagnosis guide

SymptomWhere to look first
The model keeps producing the same field incorrectlyAdd @desc and @feedback to the field; give it retry. Filter the validation events in the trace and see which rule is blowing up.
The model never calls the toolA native call depends on the model's own decision and cannot be forced. If it has to run for certain, use a direct tool.
The first response comes back malformed and the second attempt fixes itExpected behavior: when tools is defined, response_format is not sent on the first request. Give it retry.
A strict gateway is rejecting the requestIs there a map<T> in the schema? Move to the Entry[] pattern.
I expect an array in the field but a single value arrives (or the other way round)float[0..1] and float [0..1] may have been confused → shorthands.
Boolean if condition expected: …There is a non-boolean value in a logical context — ! works only on booleans, and the parts of &&/|| must be booleans as well → expression language.
'x' is not defined as a dependency.The std module alias is not one of the reserved names → import rules.
Budgets never fireThe gateway may not be returning usage; cost_per_1k_tokens may not be set. Check the trace metrics.
The tool timeout is shorter than I expectedThe effective duration is min(tool.timeout, tool_timeout_max).
vote never reaches quorumFree-text fields make exact JSON equality impossible. Use a narrow schema → vote.
The image does not reach the modelDoes the model profile have multimodal: true? Without it the marker travels as plain text.
I want to see the raw model responseRun with LUMA_DEBUG_MODEL_RESPONSE=1 — the first 4000 characters of the response are added to the error details.
try/catch around http.get never fires for a 500A status code is not an error: 404 and 500 come back as an ordinary { status, body, headers } value. Check response.status yourself → std/http.
parallel over returned fewer results than the input had elementsThat is the on_error continue contract: a failed element is skipped, not left as a hole. The only signal is the parallel_over telemetry event → the drop counter.
A failed command is not retried even though the datasource declares retryDeliberate: reads are retried, writes are not. A retried write with no idempotency key can double-apply, so the attempt count is fixed at 1 for command. Retry inside the bridge if the statement really is idempotent → datasource.
# Quick diagnosis set
luma program.luma --all --trace-file /tmp/t.json

jq -r '.events[] | select(.ok==false) | "\(.type) \(.name) \(.error)"' /tmp/t.json
jq -r '.events[] | select(.type=="validation" and .ok==false) | .metadata.rule' /tmp/t.json | sort | uniq -c | sort -rn
jq '.metrics' /tmp/t.json