Telo Invocation Contract Specification (v1.0)
0. Status, scope, and how to read this
This is a runtime conformance specification. It defines what a Telo runtime does with a resource's declared invocation contract: which declaration applies, when it is bound, which dispatch verbs it governs, how defaults are filled, what is validated, and how a violation is reported.
It is normative because the behaviour is only partly expressible in prose plus
one implementation. useDefaults is a non-standard AJV extension with
implementation-defined behaviour under anyOf and nesting; two runtimes that
guess differently would accept different manifests and fill different values, and
a manifest that works on one would silently misbehave on the other.
The key words MUST, MUST NOT, REQUIRED, SHOULD, SHOULD NOT, MAY, and RECOMMENDED are to be interpreted as described in RFC 2119.
In scope: contract resolution along extends, the mapping requirement,
contract nodes typed by the bound declaration, binding and the verbs it covers, default-fill semantics and traversal order,
validation and what it exempts, error codes and their status against throws:
unions, and ordering relative to the dispatch span.
Out of scope: the static checks a checker performs (they are diagnostics, not
runtime behaviour), the telo#Type reference grammar (see the type-field section
of CLAUDE.md), and how a controller implements the work between the two
validations.
1. What a contract is
A resource's invocation contract is two independently-declared schemas:
inputType— what a caller sends toinvoke().outputType— whatinvoke()orprovide()returns.
Both are telo#Type fields: a bare type name, a !ref to a type resource, an
inline { kind, schema }, or a raw JSON Schema. A runtime MUST accept all four
forms wherever a contract is declared.
A !ref names a type in the scope of the module that WROTE it — !ref Money is
that module's own Money, !ref Types.Money the one its Types import exports —
at kind level and instance level alike. A runtime MUST resolve it to that
declaration, never to whichever loaded module registered a type of the same name.
A contract may be declared on a kind (a Telo.Definition / Telo.Abstract
field) or on an instance (a resource that writes the property, which the kind
opts into by declaring it in its own schema). Declaring the property IS the
opt-in; a runtime MUST NOT require any additional annotation.
2. Resolution
2.1 Layering
For a given direction, a runtime MUST resolve the contract in this order and stop at the first that yields a schema:
- the instance manifest's own declaration;
- the kind's declaration, resolved per §2.2;
- undefined — the resource has no contract in that direction.
A resource with no contract in a direction MUST NOT have that direction validated. Absence is the absence of a claim, never a claim of emptiness: a runtime MUST NOT substitute a closed or empty schema.
2.2 Resolution along extends
A kind's contract is the nearest declaration along its extends chain,
self first. A definition that declares one fully replaces its ancestor's; a
definition that declares none inherits its ancestor's verbatim, at any depth.
Contracts MUST NOT be merged. This differs deliberately from the author-facing config schema and the observed-state block, which do merge: construction config and reported state are additive, a call signature is not. Merging a child's required fields into its parent's produces a union no caller can satisfy, and it would reject the one thing a child declares a signature FOR — accepting something different.
Each hop MUST be resolved in the scope that declared the definition the kind
was read off: extends aliases are lexical, and a telo#Type reference goes
through import aliases, so a chain crossing module boundaries re-scopes at every
hop.
2.3 The mapping requirement
A definition that declares its own contract while inheriting its controller
(it extends a concrete kind, declares no own controller or template body) MUST
also declare the bridging mapping:
| declared | required mapping |
|---|---|
inputType | inputs: |
outputType | result: |
Without it the inherited controller — which understands only the shape it was written for — would receive the child's shape unchanged. A runtime MUST NOT apply an unmapped replacement silently; a checker MUST reject it.
A definition with its own controller or template body is exempt: its controller IS the implementation of whatever it declares, so there is nothing to bridge.
Substitutability is unaffected by replacement. extends decides which slots
accept a resource; it never carried the dispatch contract. Whether a particular
slot may hold a resource whose contract differs from the slot's declared kind is
a wiring question, decided per slot by whether the caller can supply the
arguments at all.
2.4 Nodes typed by the declaration
A contract node MAY carry x-telo-value-schema-from: <location>. The value at
that node must then satisfy, beside the node's own keywords, every type the
location names from the declaration the contract is bound to. This is how a kind
whose instances forward caller data to resources they reference lets those
resources say what the data must be: the type is declared once, where the data
is read, and the caller-facing contract is derived from it.
The location is relative to the bound declaration and is one of:
- a field name (
contextType) — the declaration's own top-level field; - a JSON Pointer (
/providers/*/contextType, RFC 6901 escaping) — walked from the declaration's root, one segment per field, where the segment*ranges over every item of a list.
A member of the type. The annotation's object form, { from: <location>, member: <relative pointer> }, reads the location as above and holds the value
to ONE PROPERTY of each type it reaches rather than to the type: the property
whose name member yields. member is a Relative JSON Pointer from the
annotated node — a non-negative integer of levels out, then # (the key that
node is reached by) or a JSON Pointer to the string naming the property; 0# is
a map value's own key, so a map typed by a model is held key by key. A name the
type does not declare at its root contributes nothing. Inside a contract the
member is known only where it is the node's own key: a property's name, or for a
map's values every property the type declares, each under its own name; a member
any other pointer names is data and contributes nothing there.
Crossing a reference. When the value reached so far sits at one of the holding declaration's reference sites — the sites the runtime substitutes with a live instance, as its kind's schema declares them — and segments remain, the walk continues inside the declaration that reference names, to any depth. Each reference MUST be resolved in the scope of the module that declared the declaration holding it (the bound resource's own scope only for the first one): a bare name means that module's resource, an alias goes through that module's imports. A value at any other path is data, whatever its shape, and the walk continues through it as plain fields.
What a location yields. Every value the walk ends at is read as a type
field, in the forms inputType / outputType accept (a named type by
reference, an inline type, a raw JSON Schema), resolved in the scope of the
declaration holding it. The node's effective schema is the node as written,
without the annotation, plus an allOf holding every type reached — so the value
MUST satisfy all of them. Types combine by conjunction, never by merging: two
types that each close their own property set cannot both hold, and that is the
contract's answer rather than something a runtime repairs.
Each reached type is its own document. Its document-local references — #,
#/$defs/…, #/definitions/…, any #/… pointer — MUST resolve against that
type and never against the contract it types, so a type that names its own
definitions, or itself, constrains the value exactly as it does anywhere else,
and two reached types declaring a definition of the same name do not meet. A
reference to a named shape inside it resolves as written. Everything a runtime
reads off a declared node — a default: to fill (§4.2), a scalar's
representation (§4.3), a sensitive mark — it MUST read through a reached type as
through any other part of the contract.
Nothing found contributes nothing. A branch of the walk that reaches no value — an absent field, a list with no items, a value that is not a container — a value that names no type, and a declaration that does not declare the field all leave the node exactly as written. A node no branch typed is validated by its own keywords alone. A runtime MUST NOT raise for any of these: a referenced resource of a kind that declares no such field is an ordinary participant.
When. The derivation is a fact about the manifest and its references, so it
MUST be resolved against the declarations as the runtime holds them — including
a reference a static checker could not follow, such as a resource a library
receives from its importer — and MUST be in force by the first dispatch. The
node's own default: is filled before validation like any other (§4.2), so an
omitted value is judged as its default. A static checker MUST judge an omitted
value as that default and MUST NOT judge a computed value as it: an expression
may yield what the default lacks.
A location nothing can be written at is refused when the kind is registered,
with ERR_VALUE_SCHEMA_FROM_INVALID: an annotation that is not a non-empty
string or the object form with a well-formed member, a pointer with an empty
segment, a member that takes the key of a node reached by an index (or of the
root), a member pointer naming a node the schema does not declare a string,
or a location whose segments — up to
the first reference slot — name something the kind's own author schema (with
inheritance resolved) does not declare. A segment is declared when it is a
property of the node reached (through a document-local $ref, an allOf /
anyOf / oneOf branch, or a map's value schema), or * at a node declaring
items. Past a reference slot the location continues in another declaration,
whose kind the schema does not fix, and is not judged; nor is a node typed from
elsewhere. Refusing at registration covers a dependency's kind, which a
consumer's static check does not report.
The same annotation on a configuration slot of a kind's schema: reads its
location by these rules and is a static check of the value written there; it has
no dispatch behaviour. Its literals are validated against what the location
reaches, and every expression written there — an accessor's chain included —
is held to it by its type, so a misfitting expression is refused as one that does
not fit its slot. The registration refusal covers it all the same: the
location is judged wherever the kind writes the annotation — in its schema:,
its inputType or its outputType, written there or in the named shape the
field names, as that field resolves — in whichever module declares the shape,
the kind's own or one reached through an import alias, with the shape's
extends parents folded, so an annotation a parent shape declares is the
child's — and a kind carrying an unwritable one in any of them MUST be
refused with ERR_VALUE_SCHEMA_FROM_INVALID. A runtime MUST read that shape
from the declarations it holds when the kind is registered, never from created
instances. A field that resolves to no declaration is not judged; it names no
contract (§5.1). A named shape's annotation is
judged per kind, against the schema of each kind whose contract field names the
shape; a shape no kind's contract field names is not judged, nor is one reached
only by a reference from inside a contract, which is not typed either. An
annotation is read at a schema position only: a property an author NAMED
default, const, enum or examples is a schema, while the value under the
keyword of that name is data, neither typed nor judged.
3. Binding
3.1 Where
A runtime MUST bind a resource's resolved contract to its dispatch entry point at instance creation, such that a resource instance is never observable in an unbound form.
Binding at creation rather than at each handoff is normative because the handoffs cannot be enumerated safely: a reference reaches a consumer through injection into its configuration, through explicit reference resolution, through scope handles, and through the dispatch chokepoint, and a consumer that holds the instance may dispatch it directly. Enforcing at one of those and not the others leaves the contract unenforced on the others without any signal that it is.
How a runtime expresses the binding is unconstrained — a wrapped entry point, a struct field, a decorator. What is normative is the invariant above, not any particular mechanism.
3.2 Which verbs
| verb | input side | output side |
|---|---|---|
invoke(inputs, …) | bound | bound |
provide() | n/a | bound |
run(…) | not bound | not bound |
provide() takes no caller arguments, so it has no input side; its result is
validated exactly as an invocable's, by the same path.
run() is parameterless and returns nothing, so there is nothing to fill
defaults into and no result to validate. A runtime MUST NOT extend the runnable
signature to carry inputs, and MUST NOT smuggle values through the dispatch
context: that would make defaults a property each controller opts into rather
than a runtime guarantee. A resource whose resolved contract requires any input
is therefore unsatisfiable at a run dispatch site, and a checker MUST reject that
wiring.
3.3 Argument forwarding
A bound entry point MUST forward every argument it receives. The contract concerns the first argument only; later parameters carry the dispatch context (cancellation, tracing) and belong to the caller and the callee. A binding that declares only the inputs parameter silently drops that context — a detached body would never observe cancellation, and anything holding a resource across it would never be released.
3.4 Composition with an inherited mapping
Where a child both inherits a contract-bearing parent and declares its own contract with a mapping (§2.3), the bindings compose by position:
- the child's contract validates the caller's inputs;
inputs:maps them onto the parent's call;- the parent's contract validates the mapped values;
- the inherited controller runs;
- the parent's contract validates its result;
result:maps it back;- the child's contract validates the mapped result.
A runtime MUST NOT introduce a distinct wrapping object for this case. The parent instance is returned as-is and the mapping is bound to it, so the child remains a parent instance for every non-dispatch purpose — lifecycle, reported state, and any member a consumer reaches for.
4. Dispatch
4.1 Order
On a bound invoke() a runtime MUST, in this order:
- produce the effective inputs (§4.2), building each value of a type declaring
members(§4.6); - validate them against the input contract, if one resolved;
- dispatch;
- build each value of a type declaring
membersin the result, then validate it against the output contract, if one resolved.
Validation MUST happen inside the dispatch span, so a violation is attributed to the call it belongs to.
4.2 Defaults
Default-filling and input validation are one pass over the effective inputs.
The effective inputs MUST be a copy that is deep along every path a default can be written to and MAY share structure elsewhere. A flat shallow copy is non-conforming: defaults are written at every level they are declared, so a nested default would mutate the object the caller still holds. The set of such paths is derivable from the compiled schema, so the copy is bounded by the schema's defaults rather than by the size of the payload.
A default MUST be applied only where the property is absent. An explicit null
is a value, not an absence. The default applied is the one written on the
property's OWN schema — one written beside a $ref counts, one declared on the
node the reference names does not — and a checker that fills defaults before
judging a call MUST read them by the same rule.
Defaults inside composition keywords (anyOf, oneOf) are NOT RECOMMENDED in
a contract. Which branch a validator evaluates — and therefore which default it
applies — is implementation-defined across validators, so a contract relying on
it would fill different values on different runtimes for the same manifest. A
runtime MUST apply defaults deterministically for a given schema and input, and
SHOULD document the branch order it follows; authors who need a default on a
branching shape SHOULD lift it to the enclosing properties instead, where the
rule above is unambiguous.
4.3 Numeric representation
A runtime whose expression language produces a wide-integer type distinct from
its JSON number type (CEL evaluates an integer literal to one) MUST validate a
view in which those values are rendered as ordinary numbers, and MUST dispatch
the original values. A validator that does not recognise the wide type would
otherwise reject every computed integer reaching a declared integer input, for
a reason no author can act on — while converting the dispatched value would cost
a controller the full range it may need.
Defaults filled into that view are ADDITIVE, so a runtime MUST carry back only
the keys the fill added; a key the caller supplied keeps its original value. The
carry-back MUST reach values nested inside arrays: a schema may declare a default
under items, and stopping at the array boundary drops that fill silently.
4.4 Serializing a wide integer
Where a value crosses a JSON boundary — a response body, a record frame, a
persisted value — a wide integer MUST be emitted as its exact decimal digits.
JSON places no precision limit on a number, so this is lossless, and it is what a
schema-driven serializer already produces for a declared integer; a runtime with
two serialization paths MUST NOT let them disagree at any magnitude. A runtime
MUST NOT satisfy this by converting to its JSON number type, which silently loses
precision beyond 2^53, and MUST NOT emit a quoted string, which changes the type a
receiver sees.
How a runtime achieves this is its own affair: the Node kernel installs
BigInt.prototype.toJSON over JSON.rawJSON at boot, a runtime whose integers
are already a JSON number type needs nothing.
Two destinations are NAMED exceptions, because their own wire formats mandate otherwise and both are read by a receiver that cannot hold the value:
- the
otlplog encoding, whose 64-bit fields are quoted decimal strings (kernel/specs/logging.md§11.3); - the
jsonlog encoding, which degrades a value beyond the safe-integer range to a decimal string for the same reason.
A runtime MUST NOT extend this list to a general-purpose boundary. An exception is a property of the destination format, never of the value.
4.5 What validation exempts
Validation MUST skip every node whose declared value type is live, in both
directions, wherever that node appears — a property, an array item, a union
branch. Live values travel on inputs as much as on results, and the value at such
a node is a live object, not data: traversing it is the same defect as walking a
live resource instance that occupies a declared reference slot.
Exemption is a property of the TYPE, never of a position: a rule that skipped only a marked PROPERTY would leave an array-of-streams element constrained even while descending into it. It is also exemption from VALIDATION and never from TYPING — a live type's declared type arguments remain visible to every static check that reads them.
Validation MUST NOT descend into reference-typed properties for the same reason.
4.6 Declared scalars and plain-encoded text
A produced value MUST be normalized, along exactly the paths the resolved
outputType declares a scalar at, to the representation that declaration names:
a declared integer (and every json value type carrying the CEL type int) to
the runtime's wide integer, a number to its JSON number, and a value type
carrying the CEL type uint to its unsigned integer. Only an EXACT conversion is
performed — a fractional number at an integer slot, or a magnitude the target
representation cannot hold, MUST arrive unchanged so the value is rejected rather
than quietly repaired. On the way out the next reader is the expression language,
which already types the declaration. On the way in the controller MUST receive its
arguments as the call site produced them — it is host-language code — but wherever
the resource evaluates an expression over them, inputs MUST be read normalized
along the resolved inputType's declared scalar paths, without rewriting the value
the controller holds. A transport reading a value from outside the runtime against
a declared schema (a request body, query, params or headers) MUST normalize it the
same way before an expression reads it.
A value type whose representation is an INSTANCE holds that instance in both directions. Text is read into one only where the value arrives from outside the runtime, and a conforming runtime MUST decode it at exactly these sites, each against the schema the site resolves to:
- a resource's own configuration, against its kind's schema;
- an Application
variables:/secrets:env value, and thedefault:standing in for an unset one, against that entry's residual schema; - an import's
variables:/secrets:, and the librarydefault:standing in for an entry the import does not supply, against the library's declared entry — decoded when the import initializes, then validated against that entry; - every slot a resource writes whose schema comes from ELSEWHERE — a call's
argument map (a step's
inputs:, the map a reference slot names through itsinputs:pointer, a template definition's top-levelinputs:, a boot target's inline step), anx-telo-schema-fromslot and anx-telo-value-schema-fromslot — decoded when the resource holding them is CREATED, not per dispatch; - a value a transport receives — an HTTP request's body, query, path parameters
and headers — against the schema its route declares, before the handler sees
it. The runtime offers controllers this decoding as a context service
(
ResourceContext.readPlainEncodedin Node), which also refuses text the encoding does not read withERR_INPUT_INVALID; a transport documents and validates such a slot as the TEXT a client sends (type: string, format: date-timefor a timestamp).
The decoding MUST use the type's declared plain encoding and nothing else, and
text the encoding does not read MUST be left as written so the slot's own
assertion refuses it — with the same message the static checker produces —
wherever the runtime validates that slot: a configuration at creation, an
argument map at its dispatch. A slot whose schema is DERIVED from another kind's
(x-telo-schema-from, x-telo-value-schema-from) is decoded at creation and
asserted statically only, since no runtime reader validates a slot against a
schema borrowed from another kind. A COMPUTED value is never decoded (it must
already be the instance), and neither is text an embedding tag produced: a
runtime MUST decode before it resolves embeds, so a file's contents are never
read as an encoding.
A value type declaring members (Telo.Url) is BUILT, not only decoded: its text,
or an object of its members, becomes the instance, held to the slot's type
arguments. Beyond the sites above, a conforming runtime MUST build such a value in
BOTH directions of a bound contract — a call's arguments before they are validated,
a result before it is validated — since an expression may produce the text or the
object at either, and a transport's inbound value through the same context service.
A value built at a contract boundary joins no redaction set — a set grows by one
text per distinct value, and a dispatch site sees a new value per call — and is
protected by the value rule of the logging spec (kernel/specs/logging.md §14),
which every observability writer applies wherever the value is written. One that
does not build is the TYPE's
refusal, but at a contract boundary it is never raised in place of the direction's
code: it is carried INSIDE ERR_INPUT_INVALID / ERR_OUTPUT_INVALID (§5.1), as
an entry of the error's data, so the set of codes a dispatch can raise stays the
closed ambient union whatever value types exist.
A HOST PATH (Telo.HostPath, a json value type declaring fromHost) is
absolute wherever it is held, and the x-telo-type assertion refuses a relative
one — judged host-neutrally, so a POSIX path, a drive-letter path and a UNC path
are absolute on every host. The one site a relative host path is READ is the
Application variables: / secrets: site above: a conforming runtime MUST
resolve every host path in that value against the entry's fromHost anchor
(working-directory: the process working directory) before validating it. A
compile-eval expression at a host-path slot is only a placeholder when the
configuration is validated, so its RESULT MUST be refused at creation when it is
relative (ERR_HOST_PATH_RELATIVE). !module-path resolves to an absolute path
at creation, beside the embeds.
4.7 Writing for a reader outside the runtime
A value written where the reader is not a Telo runtime — a transport body and its
OpenAPI document, a log line (json, pretty, otlp), a debug-wire trace
payload, a runner session event, a CLI JSON document — MUST be written in the
PLAIN encoding, keyed on the value, and MUST NOT carry the typed frame's $telo
tag (durable-execution.md §6), whose readers are Telo runtimes alone:
- a timestamp as RFC 3339 text in UTC, a duration as seconds (
"5400s"), bytes as base64url without padding — each value type's declaredencoding; - a
uintas its exact decimal digits, as §4.4 writes an int; - NaN, +Infinity and -Infinity as the strings
"NaN","Infinity"and"-Infinity"(the protobuf JSON mapping, which OTLP/JSON also follows), and a negative zero as0(RFC 8785); - a map whose keys are not all strings as an object keyed by each key's text
(
1,true). Two keys whose text is one —1and"1"— MUST be refused rather than one of them dropped.
A reader recovers the type from the schema it was promised. Two destinations keep
their own rendering of bytes, because their formats define one: a log record's
bytes attribute (base64, or a blob pointer on the debug wire — logging.md
§6.1), and a debug-wire payload's byte buffer, offloaded to its blob store.
5. Errors
5.1 Codes
| code | raised when |
|---|---|
ERR_INPUT_INVALID | inputs did not satisfy the resolved inputType |
ERR_OUTPUT_INVALID | a result did not satisfy the resolved outputType |
ERR_CONTRACT_UNRESOLVABLE | a declared contract resolved to no schema at all |
ERR_VALUE_SCHEMA_FROM_INVALID | a kind's x-telo-value-schema-from names a location nothing can be written at (§2.4); raised at registration |
A declared contract that cannot be resolved — a named type that never registered — MUST raise rather than degrade to "unvalidated". Silently disabling enforcement is the failure mode nobody notices: every later call passes because nothing is checking. It is a distinct code because nothing is wrong with the data; the contract itself is unusable.
A violation MUST be raised as a structured error carrying its code, not a plain one. A runtime that assigns a generic code to unstructured failures would otherwise make a contract violation indistinguishable from a crash inside a catch block, leaving an author unable to match it or to rethrow it faithfully.
The message MUST identify the target, the direction, and the offending detail. A caller several steps away cannot otherwise tell which boundary rejected the value or which side supplied it.
A value type's own refusal at a contract boundary — a value of a type declaring
members that does not build (§4.6) — MUST be raised as the direction's code, never
as the type's own: the type's code (its acceptsText.code, ERR_-prefixed, e.g.
ERR_URL_INVALID) is data an entry declares, and raising it at dispatch would widen
what every dispatch can throw with every new value type, beyond what any catches:
list, coverage check or retry exclusion can model. The error's data MUST carry one
entry per value that did not build, under issues, each holding its pointer (a
JSON Pointer into the inputs or the result, /url), the type's code and its
reason, and — for a transport's answer — the dotted path and a message; the
rendered text MUST carry the same entries (/url: ERR_URL_INVALID: not a Telo.Url — <reason>). Neither MAY quote the value, which may hold a credential. The same
entry is what an application's ERR_MANIFEST_VALIDATION_FAILED carries for an
environment value or a default: of the type; at a resource's creation, at an
import's field-by-field need supply and at an import's variables: / secrets:
(§4.6) the type's code is raised as itself, naming the import and the variable
there. Any other mismatch between an import's supply and the library's declared
entry is ERR_RESOURCE_SCHEMA_VALIDATION_FAILED (a relative host path
ERR_HOST_PATH_RELATIVE), naming the import, the variable and the failed keyword,
rendered from the failure's issue rather than from a message quoting the value.
5.2 Status against declared unions
These codes, and ERR_FUNCTION_FAILED (§7.3), form an ambient union: they are
raised by the runtime for every kind alike, not declared by any kind.
- A
catches:entry MAY name one, and a checker MUST accept it and MUST type-check it against this set exactly as it does a declared code. - A kind's declared-throws completeness rule MUST NOT count them. Folding them into every union would make every bounded catch block incomplete at once.
- A kind MUST NOT declare either code in its own
throws:. The contract is enforced in one place, so declaring it in a second would describe the same failure twice and diverge. - A retry policy MUST NOT re-attempt one: each is a verdict on the shape of the call, or on a synchronous call that fails the same way again.
5.3 A declared union as a ceiling
A kind's throws: is the third part of its contract. A Telo.Abstract MAY
declare one, as a literal codes list only (inherit and passthrough need a
body, which an abstract does not have, so neither key is admitted), and only
where its capability is dispatchable.
- The nearest ancestor along
extendsthat declares a literalcodeslist, abstract or concrete, is the ceiling. A definition whose literal codes include one the ceiling does not MUST be refused at registration (ERR_THROWS_NOT_SUBSTITUTABLE), and a checker MUST report it (THROWS_NOT_SUBSTITUTABLE). - A descendant's own list replaces and never merges, as §2.2 requires of every contract part. The per-dispatch undeclared-code check against the implementation's own list makes the ceiling hold transitively.
- An ancestor whose block is
inheritorpassthroughbounds nothing of its own and is looked past, so a literal list above it still bounds everything below it. A definition whose own block is one of them is not judged at registration. - The shape of a
throws:block, and which declared capabilities may carry one (Telo.InvocableandTelo.Runnable; any other declared capability, a third party's included, may not), MUST be the same rule for a checker and a runtime. - A checker resolving what a dispatch MAY throw through a target known only by its kind (a library's resource input) MUST read the nearest declared list along that kind's chain, and MUST treat a chain declaring none as unbounded, never as empty.
6. Conformance
A conforming runtime:
- resolves contracts per §2, nearest-declaration and never merging;
- rejects an unmapped replacement on a controller-inheriting definition (§2.3);
- binds at creation so no instance is observable unbound (§3.1);
- binds
invokeandprovideand notrun(§3.2); - forwards every dispatch argument (§3.3);
- fills defaults over a copy deep along default-bearing paths (§4.2);
- validates both directions, exempting streams in both (§4.5), over a view where wide integers read as numbers (§4.3);
- serializes a wide integer as exact decimal digits at every JSON boundary but the named log encodings (§4.4);
- raises the ambient codes structurally, and excludes them from declared-union counting (§5);
- raises rather than silently skipping when a declared contract resolves to no schema (§5.1);
- refuses a definition whose literal throw codes exceed its ancestor's ceiling (§5.3);
- binds module calls per module scope at creation, refusing and deferring as §7.1 states, and evaluates them as §7.2 and §7.3 state;
- binds every function's
calland hands a native function's controller the function context, as §7.4 states; - types a contract node carrying
x-telo-value-schema-fromby every type its location reaches, resolving each reference crossed in the scope of the module that declared its holder, leaves a node nothing typed as written, and resolves each reached type's document-local references against that type, and refuses an unreachable location at registration in every position a kind writes the annotation — in itsschema:, itsinputTypeor itsoutputType, written there or in the named shape the field names, resolved across imports and with itsextendsparents folded (§2.4).
7. Module functions
A callable resource (capability Telo.Callable) is reached from a CEL expression
through a module name — Self.fn(…), <ModuleName>.fn(…) or <Alias>.fn(…) —
and never dispatched. This section is what a runtime does with such a call. A
runtime that evaluates no CEL hosts no functions and is exempt.
7.1 Binding
A runtime MUST bind every module call a resource's manifest makes when it creates that resource, before any of the resource's expressions evaluate — including a call in a branch that never runs. The call resolves in the module whose names it was written with: for a template body, the module that DEFINED the template.
Self.<name>/<ModuleName>.<name>names a resource that module declares;<Alias>.<name>one the import's library lists inexports.resources.- A name no resource answers to is refused with
ERR_FUNCTION_UNRESOLVED, a declared but unexported one withERR_FUNCTION_NOT_EXPORTED, and a resource whose capability does not resolve toTelo.CallablealongextendswithERR_FUNCTION_NOT_CALLABLE. - A callee that exists but has not initialized MUST defer the caller as a pending
reference does (
ERR_LOCAL_REF_PENDING, orERR_CROSS_MODULE_REF_PENDINGthrough an import), and the call is a dependency of the caller for ordering, failure attribution, teardown and reconciliation. A body that calls itself, directly or through another function, is therefore a dependency cycle, and MUST be refused withERR_CIRCULAR_DEPENDENCYbefore any resource in it is created.
Only a created resource's manifest binds. An expression evaluated outside one — an
Application's logging: block, resolved while the application loads, and a type
rule's condition, evaluated against the value alone — has no function in scope,
and a call there fails.
A binding is made once per module scope per qualified name — two isolated imports of one library bind twice, a shared one once — and evaluating a call MUST NOT resolve it again. When the callee is withdrawn its binding MUST be dropped, and an evaluation that still reaches it fails as a cancellation.
7.2 Arguments
A call site is positional and a callable receives one object keyed by parameter
name, mapped through the signature in force for the callee. An omitted optional
parameter takes its schema's default, normalized to the representation the
schema declares (§4.6), and null when it declares none. An argument count the
parameter list does not accept MUST be refused with
ERR_FUNCTION_ARITY_MISMATCH, never truncated or padded — a structured error a
try: step can catch, outside the ambient union.
A Telo.Function body MUST be evaluated with its parameters and the module's
function bindings in scope and nothing else: no module inputs, resources, ports,
module metadata, steps or request.
7.3 Errors
Whatever a callable throws MUST fail the evaluation as ERR_FUNCTION_FAILED, a
structured error whose data carries function (the qualified name as written),
the thrown message, and its code when it had one — except an error that already
is ERR_FUNCTION_FAILED, which names the function that actually failed, a
cancellation or a durable suspension, and a function's binding refusals (§7.4),
which all pass unchanged. A holder calling a function it holds through a slot
gets the same classification, with function naming the instance as
<kind>/<name>.
A failed expression MUST keep a coded error's code and data: a runtime that
rewrapped them into an uncoded failure would make ERR_FUNCTION_FAILED
unreachable to a try: or a catches: list.
7.4 Binding a function's call
A runtime MUST bind every callable instance's call to the signature in force for
it when it creates the instance — a Telo.Function as well as a native function —
so no holder reaches it unbound. An instance a kind inherits its controller for IS
its ancestor's instance, bound when that was created, and is not bound again.
- Before each call it fills defaults (a fresh copy per call) and normalizes
declared scalars as §7.2 states, and refuses arguments the parameters do not
admit with
ERR_INPUT_INVALID. A promise-like result isERR_FUNCTION_ASYNC; any other result is normalized and validated againstreturns, refused withERR_OUTPUT_INVALID. These three carryfunctionin their data and pass unwrapped (§7.3). A refused promise that later rejects is reported, never left unobserved. - A CEL call binds its positional arguments to parameter names and hands them to the bound call, so a call is bound and validated once.
- An instance with no synchronous
call(args)MUST be refused withERR_CONTROLLER_INVALIDnaming the function as<kind>/<name>.
A callable kind whose definition declares no body field (no property annotated
x-telo-returns-from) and does not inherit its controller is NATIVE: its result
comes from its controller's code.
- Its controller's
create(resource, ctx)MUST receive a function context offering exactlyresolveControllerFile,resolveNativeFile,logandeffect— no environment, no resource, no I/O. Allocationscreatemakes are registered througheffectand join the resource's create frame, so teardown and reload release them. A kind inheriting a native controller creates its ancestor's instance through the ordinary context, and the ancestor's controller receives the function context. - A controller exporting no
createMUST be refused withERR_CONTROLLER_INVALIDnaming the function as<kind>/<name>. - Arguments arrive as CEL values: an integer as a 64-bit integer, a timestamp or
duration as the runtime's native value, bytes as the runtime's byte type, and a
Textargument as its text record (§7.5). - A function whose code runs in another language's runtime receives its
configuration as the resource's plain JSON and its arguments, and returns its
result, as typed frames (
durable-execution.md§6), so no value changes type crossing that boundary. A panic there fails the evaluation asERR_FUNCTION_FAILEDcarrying the codeERR_CONTROLLER_PANIC. Its instance is destroyed as the inverse of its creation, at teardown and on reload.
7.5 Text parameters
A parameter whose schema is the built-in Text fragment
(telo://manifest#/$defs/Text) takes a text written AT THE CALL.
- When it binds a call (§7.1), a runtime MUST refuse an argument at such a parameter
that is not written as literals alone with
ERR_TEXT_ARGUMENT_COMPUTED, and MUST judge the literal text in thelang:of the module the call is bound in —ERR_MESSAGE_SYNTAX_INVALID,ERR_MESSAGE_SOURCE_LANGUAGE_MISSING,ERR_MESSAGE_PLURAL_CATEGORY_INVALID. - A parameter whose schema carries
x-telo-text-values-for: /<name>holds the values of theTextparameter<name>. Every placeholder and selector the text reads MUST be a key of them (an omitted values argument is an empty map), each value a string or a number, a selector's a number — refused withERR_MESSAGE_PLACEHOLDER_UNBOUND/ERR_MESSAGE_PLACEHOLDER_VALUE_UNSUPPORTED: when the call is bound for values written as literals, and before the function runs (§7.4) for values an expression computes. - A NATIVE function receives each
Textargument as the record{ text, context, lang, computed }:textthe message as written,contextits context ornull,langthelang:of the module the call is bound in (undwhen it declares none) andcomputed: false. A holder calling a function through a slot it holds names no module, so the text iscomputed: trueinund. ATelo.Functionbody reads the text as written, as its parameters are typed. - The argument record a call is validated against is one schema of the parameters, each parameter's schema a root of its own whose shared fragments resolve inside it.
A runtime that builds no text records MUST refuse a resource whose kind's schema or
parameters reach the Text fragment (ERR_UNSUPPORTED_MANIFEST_FEATURE) rather than
hand its controller a raw message. Polyglot debt: kernel/rust is such a runtime.
7.6 Texts a controller evaluates
A field a controller evaluates after creation — one its kind marks
x-telo-eval: runtime, or one a CEL region covers — may hold or produce a Text or a
Message. A runtime MUST settle what such an evaluation produces exactly as it settles
a resource's texts at creation:
- which expressions bear text — one at, below or above a
Text/Messageslot, and one half of a values pairing — and thelang:of the module that wrote each are fixed when the instance is created, forwarded values carrying their writer's; - each text produced is judged in that language (
ERR_MESSAGE_SYNTAX_INVALID,ERR_MESSAGE_SOURCE_LANGUAGE_MISSING,ERR_MESSAGE_PLURAL_CATEGORY_INVALID) and aTextis handed back as its record withcomputed: true; a literal already settled at creation is handed back as it is; - a values pairing the evaluation completes is judged against its text
(
ERR_MESSAGE_PLACEHOLDER_UNBOUND,ERR_MESSAGE_PLACEHOLDER_VALUE_UNSUPPORTED).
This holds in process and across the controller boundary (Value.Expand). An
x-telo-eval: accessor field is never evaluated, so a kind whose schema types a text
at or beneath one is refused when it is registered (ERR_TEXT_SLOT_ACCESSOR); a values
pairing whose values are accessor fields is read as their bindings — a literal's
{ value } as that value, a chain's { root, path } as a key present with a value its
consumer resolves.