Skip to content

Telo v0.92.0

Choose a tag to compare

@github-actions github-actions released this 17 Sep 18:08
8f12158

Packages

Package Version
@telorun/cli 0.92.0
@telorun/analyzer 0.76.0
@telorun/ide-support 0.21.0
@telorun/kernel 0.92.0
@telorun/sdk 0.92.0
@telorun/templating 0.21.0

Minor

telo cel functions <manifest> lists, ahead of the catalog, the module functions that manifest can call — its own through Self and each import's exported ones through the alias — with their signature, description and derived determinism, naming the chain to a non-deterministic or host-backed leaf. Under --json each is an entry of category module carrying deterministic, hostBacked, nondeterministicVia and hostBackedVia wherever the analysis derived them, and none of the four where it did not. The manifest is loaded exactly as telo check loads it — through the .telo/manifests cache, with mutable oci:// tags revalidated — and one that does not load, or that analysis reports errors in, is refused on stderr, each error located, with a non-zero exit. @telorun/ide-support exports functionSignature, the one rendering of a function's signature every surface shares. — cli, ide-support

Every boundary read outside Telo writes a CEL value in its plain encoding, never type-tagged. @telorun/sdk exports the writer — toPlainJson, writePlainJson, plainScalar, plainMapKey — and plainSchemaOf, the schema of the text an instance-typed slot is written as; each plain encoding now carries that schema. A timestamp is RFC 3339 text in UTC, a duration is seconds ("5400s"), bytes are base64url, a uint is its digits, NaN and ±Infinity are "NaN" / "Infinity" / "-Infinity", a negative zero is 0, and a map with int or bool keys is an object keyed by their text; a map two of whose keys share one text is refused with ERR_PLAIN_JSON_UNWRITABLE. — cli, kernel, sdk

Log attributes (json, pretty and otlp), debug-wire payloads and CLI JSON documents (-o json, telo cel eval --json) now write a duration, a uint and an int-keyed map in that form, where they used to write an empty object or a [Duration] marker; a debug-wire payload holding a map whose keys share a text or are not CEL map keys writes it as its [key, value] pairs rather than failing the call it observes. ResourceContext.readPlainEncoded(value, schema) decodes a value that arrived from outside — a transport body — at every slot declaring a plain-encoded value type, through the walk a YAML literal is read with, and refuses text the encoding does not read with ERR_INPUT_INVALID, whose data.issues lists each refusal as { path, message }.

A diagnostic's repair can be a tagged scalar. DiagnosticFix gains an optional tag (ref | cel), carried by telo check -o json (fix.tag), by CheckDiagnostic on the SDK's runtime seam, and by ide-support's replace suggestion; renderFixReplacement takes it as a third argument and writes !ref <replacement> or !cel "<replacement>", so the VS Code quick fix now writes the tag instead of quoting it into the value. — cli, analyzer, ide-support, kernel, sdk

FUNCTION_TYPE_NAME_FORM now repairs schema: Money as { replacement: "Money", tag: "ref" } rather than the untaggable value !ref Money. INVALID_REFERENCE_FORM gains a repair for a bare-name string reference (handler: onMessage → !ref onMessage) — not for a dotted FQN or the { kind, name } object, whose target would be a guess — and an untagged rule condition: (RESOURCE_RULE_INVALID / REFERRER_RULE_INVALID) is repaired by tagging the same expression !cel.

Functions become declarable. Telo.Callable is a new capability — a synchronous call(args), deliberately outside Telo.Executable, so a step's invoke: naming one is refused by telo check (REFERENCE_KIND_MISMATCH) and by the kernel (ERR_RESOURCE_NOT_INVOKABLE). Telo.Function is a new built-in kind for a function written in CEL: params, returns and a required body: !cel "…", with a closed schema, so deterministic: on one is a SCHEMA_VIOLATION. A body is typed against its parameters, so value.amont is CEL_UNKNOWN_FIELD; the new x-telo-context-parameters-from annotation is what brings an ordered parameter list into a field's CEL scope, and it REPLACES the kernel globals there — a body reading variables, secrets, resources, ports, module, steps or request is CEL_UNKNOWN_IDENTIFIER, while catalog functions and module calls stay reachable. — analyzer, kernel

A callable Telo.Definition or Telo.Abstract declares a signature — params (an ordered list of { name, schema, description?, nullable?, optional? }) and returns ({ schema, description?, nullable? }) — layered like an invocation contract: the instance, then the nearest declaration along extends, replacing rather than merging. schema is JSON Schema in which !ref <Shape> resolves at the root and at any depth, on a kind document as on an instance; the rest of a kind document keeps its references unresolved. A native kind may claim deterministic: true when it declares its own controllers:, and a callable abstract may require it.

New diagnostics: CALLABLE_DEFINITION_INVALID (a callable kind holding a non-callable, non-type slot; declaring x-telo-eval: runtime, status:, a zone annotation, x-telo-scope, a step body or a template body; extending Telo.Function; deterministic non-boolean, declared without its own controllers:, or claimed over a slot whose abstract does not require it; a controller-inheriting child redeclaring a signature; a signature or deterministic on a kind that is not callable), X_TELO_REF_CALLABLE_UNTYPED (a slot constrained to bare Telo.Callable), FUNCTION_OPTIONAL_NOT_TRAILING and FUNCTION_TYPE_NAME_FORM (a bare shape name, repaired with !ref). The kernel refuses the same kinds at registration with ERR_CALLABLE_DEFINITION_INVALID, reading the analyzer's rules. CONTRACT_NOT_SUBSTITUTABLE extends to signatures replacing a callable abstract's: parameters contravariant and positional, the result covariant, arity, and determinism the abstract requires. x-telo-sensitive inside a signature stays SENSITIVE_ANNOTATION_MISPLACED. A slot constrained to a function kind that declares no signature of its own — Telo.Function among them — is X_TELO_REF_CALLABLE_UNTYPED like bare Telo.Callable. A !ref in a signature that names nothing, at the root or nested, is CONTRACT_TYPE_NOT_FOUND, refused by the kernel at registration too. An instance takes params / returns as its signature only where its kind's schema declares them, and deterministic on an instance of a function kind is CALLABLE_DEFINITION_INVALID. Parameter names go through the naming rules (INVALID_NAME, NAME_CASE_CONVENTION). The kernel's ERR_CALLABLE_DEFINITION_INVALID message names each clause by its analyzer code and path.

A kind's inputType / outputType naming a shape with !ref now resolves in the module that declared the kind — it was left unresolved, so the kernel refused the contract at dispatch with ERR_CONTRACT_UNRESOLVABLE while telo check said nothing — and one naming nothing is CONTRACT_TYPE_NOT_FOUND. A consumer's analysis now carries a library's own shapes that the library's contracts and signatures name, so CONTRACT_NOT_SUBSTITUTABLE compares a child against an imported abstract's named shapes rather than passing by comparing nothing. throws: on a callable is refused by THROWS_ON_NON_DISPATCH_CAPABILITY and the kernel's capability branch.

The kernel binds a contract's !ref <Shape> to the shape in the module that wrote the reference, on a kind and on an instance. It looked the shape up by its bare name, which every module declaring a shape of that name registers, so a library declaring Money and importing a library that exports another Money had its results checked against the import's — ERR_OUTPUT_INVALID on a value telo check accepted. A shape's CEL rules: apply through the same resolution.

A Telo.Abstract with extends now defers registration until its ancestor is loaded, as a Telo.Definition already did, so one whose extends names no kind fails at boot; telo check reports it at the abstract as EXTENDS_MALFORMED / EXTENDS_UNKNOWN_TARGET, which it previously checked on definitions only.

Timestamps, durations and unsigned integers join the value-type vocabulary. x-telo-type: Telo.Timestamp and Telo.Duration hold CEL's native timestamp and duration (typed google.protobuf.Timestamp / google.protobuf.Duration by telo check); Telo.Uint64 is an integer CEL reads as uint, and a declared output normalizes to one. A new catalog function now() returns the current instant as a timestamp (non-deterministic). — analyzer, kernel, sdk, templating

Every non-live instance value type declares its plain encoding — Telo.Bytes base64url without padding, Telo.Timestamp RFC 3339 (written in UTC Z), Telo.Duration any CEL duration string within CEL's range of ±315576000000s (written as seconds, 5400s). A string literal is decoded from it wherever a value arrives from outside: a resource's own config slot, an Application variables: / secrets: env var or default:, and every slot typed from elsewhere — a call's argument map (a step's inputs:, a reference slot's inputs: pointer, a template's top-level inputs: — read against its body entry's contract with any named shape in it resolved in the declaring module, at the root, nested or under $defs — a boot target's inline step), an x-telo-schema-from slot and an x-telo-value-schema-from row. One reader enumerates those sites for telo check and for kernel creation alike, so a literal is read once against one schema; text the encoding does not read is left as written, so telo check refuses it at every one of those sites and the runtime refuses it again wherever that slot's own assertion runs — a config slot when the resource is created, an argument map when the call is dispatched. A computed value is never decoded, and neither is an embed's text — the kernel decodes before !include-* resolves. A Telo.Uint64 value outside 0…2^64-1 is refused (SCHEMA_VIOLATION / ERR_OUTPUT_INVALID), and so is a plain number beyond 2^53 − 1, which is already some other integer — the message says to write it as a CEL uint (!cel "18446744073709551615u"); @telorun/sdk exports markExactRendering, with which a host validating a BigInt-normalized view marks the doubles it rendered from exact integers so they are not refused, a CEL expression may fill an instance-typed slot (its stand-in is the instance), an instance's CEL type no longer satisfies a JSON-typed slot and vice versa, and integerInput accepts a uint.

@telorun/sdk now depends on @marcbachmann/cel-js, pinned to exactly 7.6.1, and exports Duration and UnsignedInt plus PLAIN_ENCODINGS, CEL_SCALAR_FORMS and decodePlainText; templating, analyzer and kernel pin the same version. @telorun/templating refuses to load when its CEL engine is a different copy than the SDK's, and the kernel refuses to build a controller bundle that inlines @marcbachmann/cel-js. @telorun/analyzer gains derived-slots.ts — the one reader of a resource's call-argument maps and schema-derived slots, consumed by the step-inputs, schema-from and value-schema-from checks and by kernel creation (AnalysisRegistry.derivedSlotsOf). The kernel's compiled-validator cache key now covers the value-type vocabulary, so a validator baked before a vocabulary change is recompiled rather than served stale.

telo check reports a loop in the resource dependency graph as DEPENDENCY_CYCLE (error), one diagnostic per loop, anchored on a resource in it and tracing the loop in the same wording as the kernel's ERR_CIRCULAR_DEPENDENCY. Only loops with a resource in the entry's own modules are reported. Because telo run analyzes before booting, an application whose own resources form a loop now fails at load with this diagnostic instead of at boot. — analyzer

A function's determinism is derived. A function written in CEL is deterministic and host-free exactly when everything it calls is — catalog functions and other module functions alike — and a native function is always host-backed and deterministic only where the kind supplying its controller declares deterministic: true. Every message about it names the chain to the leaf that decided it (Billing.isStale → now()). A module call's CallSite carries both flags, supplied through the new AnalyzeEnv.moduleCallFlags. — analyzer, kernel, templating

The consumers read the derived flags: DURABLE_NONDETERMINISM inside an idempotent region, the CEL_NONDETERMINISTIC_IN_COMPILE_FIELD warning, and rule conditions. A rule condition (x-telo-resource-rules, x-telo-referrer-rules) may now call a deterministic function written in CEL, which telo check evaluates; a call reaching a non-deterministic or host-backed leaf is refused naming the chain, and the refusal follows an edit to the body with no edit to the rule. A consumer's analysis now holds the private functions a library's exported functions call, so what those reach is derived rather than guessed.

A function satisfies a slot constrained to a callable abstract by extends or by its signature: results covariant, parameters contravariant, by position and by name — a holder calls it with the names the abstract declares — and, where the abstract requires it, determinism. A mismatch is REFERENCE_KIND_MISMATCH naming the parameter or the chain.

Module calls are first-class in the editor. Completion after Self., a module's own name or an import alias offers the functions that call can name, each with its signature; hover on a call shows the function's signature, description and derived determinism, naming the chain to a non-deterministic or host-backed leaf; the new buildSignatureHelp highlights the argument being written; go-to-declaration jumps from a call to the function resource (and from its alias to the import); and a call's receiver is coloured as a namespace. Renaming a function renames its calls, and renaming a named shape proposes a type-level name — buildRename takes the analysis, and ManifestAnalysis.nameLevel answers whether a resource's name denotes a type or a value. — analyzer, ide-support

CelScope gains moduleFunction and moduleFunctionsOf. The module graph draws each module call as a holds edge from the calling resource to the function (slot: cel, the function named in call), and a shape a contract or signature names (params[0].schema: !ref Money) is a schema reference in the call graph, so it is a shape edge rather than an untyped one.

telo check judges a module call against what it reaches, as the kernel does when it binds it. FUNCTION_UNRESOLVED now means a name no resource answers to — through Self or the module's own name among the module's own resources (never a with: scope's), through an alias among what the imported library declares — and says why. A private one is FUNCTION_NOT_EXPORTED, a resource that is not a function FUNCTION_NOT_CALLABLE, a count the parameter list does not accept FUNCTION_ARITY_MISMATCH, and an argument whose CEL type — or, for an argument naming a value, whose declared shape — does not fit its parameter FUNCTION_ARGUMENT_MISMATCH. All are entry-module-scoped. — analyzer, kernel, templating

A call types as the callee's declared returns, so an operator over it is checked and a member read off it is checked against the result's schema (CEL_UNKNOWN_FIELD). A Telo.Function body must produce its declared result (FUNCTION_RETURN_MISMATCH, read through the new x-telo-returns-from annotation); its parameters see through named shapes at any depth, and a nullable parameter, or an optional one with no default, must be guarded (CEL_NULLABLE_ACCESS). A template body's calls resolve through the module that defines the kind. A function is absent from the resources scope, since it publishes no reading. A module call in an import's variables: is reported once, at the import.

A module call where the runtime binds no function is refused where it is written: FUNCTION_CALL_UNBOUND, declared by the new x-telo-unbound-calls annotation on an Application's logging: block (resolved while the application loads) and on a Telo.JsonSchema rule's condition (evaluated against the value alone).

A module call is a dependency edge in the call graph, so boot creates a callee before its caller, and a function calling itself, directly or through another, is DEPENDENCY_CYCLE in telo check and ERR_CIRCULAR_DEPENDENCY at boot, where it used to defer until the init loop gave up. The module graph does not draw these edges.

In the kernel, a wrong argument count is a structured ERR_FUNCTION_ARITY_MISMATCH a try: step can catch, and a durable suspension raised inside a function reaches its workflow unwrapped rather than as ERR_FUNCTION_FAILED. Each argument and the result of a module call are normalized to the scalars the signature declares, exact conversions only, so an int passed where a parameter declares type: number reaches the body as the double it was typed against. A call to a function an import re-exports resolves through the export table, as it runs.

CEL resolves a call whose receiver is one of the declaring module's names — its imports: keys, Self, its metadata.name and Telo — into a late-bound module call, carrying the qualified name as written. A bare name is still always the core catalog, so format(…) and Billing.format(…) never collide, and a.b.c(x) is an ordinary method call. Resolution happens on the parsed tree at compile, so diagnostic ranges and the expression a trace shows stay the author's; a partial compiles with the names of the module including it, and the loader's parse cache is keyed on them. — analyzer, sdk, templating

Every CEL analysis reads the resolved tree rather than re-parsing: access chains, a compiled value's refs, binding dependency order, module-graph data edges, scope-reach and the unused-declaration walk never take a module name for a root identifier; CEL_UNKNOWN_FUNCTION / CEL_WRONG_CALL_FORM skip a module call, and no catalog determinism or host-backed flag is attached to one. CompiledValue gains calls, the expression's qualified calls, beside refs. The analyzer registers a module's own metadata.name as an alias beside Self, which the kernel already did.

Evaluating a module call that nothing bound fails with unbound function '<qualified>'. A rule condition calling one is refused at the declaring kind, since a rule is evaluated where no module's functions are bound.

BINDING_NAME_RESERVED extends to a comprehension variable or cel.bind name equal to one of the module's names, and to an x-telo-bindings-from key equal to one. An import alias a kind already puts in CEL scope is IMPORT_ALIAS_SHADOWS_CONTEXT, reported at that imports: key. CEL_UNKNOWN_IDENTIFIER on a call receiver says a module call needs an imports: alias, where the name could denote a module at all.

Functions run. The kernel creates Telo.Function resources and any other callable resource, and binds every module call a resource's manifest makes — Self.fn(…), <ModuleName>.fn(…), <Alias>.fn(…) — when it creates that resource, before its compile-time fields evaluate, once per module scope per name. A call then evaluates wherever an expression does: a compile-time field, a step guard, a step, a template body (which calls through the library that defined the template). Two isolated imports of a library bind to two instances, a shared library to one. A Telo.Function body sees its parameters and the functions its module can call, nothing else; an omitted optional parameter takes its declared default or null. A function publishes no reading, so resources.<name> does not contain it. — analyzer, kernel, sdk, templating

A call is refused when its resource is created: ERR_FUNCTION_UNRESOLVED (nothing by that name), ERR_FUNCTION_NOT_EXPORTED (the imported library keeps it private), ERR_FUNCTION_NOT_CALLABLE (a resource that is not a function); a callee not yet initialized defers the caller like a pending reference, and a wrong argument count is ERR_FUNCTION_ARITY_MISMATCH. A function resource with a signature holding a required parameter after an optional one or a shape named as a bare string, a function named after a CEL macro, or an instance of a native function kind declaring deterministic, is refused at creation with ERR_CALLABLE_DEFINITION_INVALID, as telo check reports it. A Telo.Function declaring deterministic fails its kind's schema (ERR_RESOURCE_SCHEMA_VALIDATION_FAILED): a body's determinism is derived, never claimed.

Whatever a function throws fails the expression as ERR_FUNCTION_FAILED, carrying the function's qualified name and the thrown code and message. It is a new ambient code (@telorun/sdk exports ERR_FUNCTION_FAILED in AMBIENT_CONTRACT_ERROR_CODES): a try: or catches: can catch it, and a retry policy does not re-attempt it. A failed expression now keeps a coded error's code and data instead of rewrapping it as an uncoded error.

A call is a dependency of the resource making it, so editing a function in the entry module rebuilds its callers on reconcile; a kind definition in the impact set now reports restartRequired, since a kind registers once per kernel. @telorun/templating exports MODULE_CALL_DISPATCH_KEY and ModuleCallDispatch; @telorun/analyzer exports callableInstanceIssues.

A native function has a contract. @telorun/sdk exports FunctionController, FunctionInstance, FunctionContext and ERR_FUNCTION_ASYNC: a callable kind's controller receives a context offering only resolveControllerFile, resolveNativeFile, log and effect, and returns an instance whose call(args) is synchronous — a call returning a promise fails the TypeScript build. A kind that extends a native callable kind and inherits its controller, with base: or by merge, creates its ancestor's instance as any inheriting kind does. — analyzer, kernel, sdk

The kernel binds every function's call — a Telo.Function as well as a native one — to its signature when it creates the instance, so every holder reaches the bound one: defaults fill (a fresh copy per call), declared scalars normalize, and arguments and result are validated (ERR_INPUT_INVALID / ERR_OUTPUT_INVALID); anything else the function throws is ERR_FUNCTION_FAILED naming it as <kind>/<name>. A CEL call is bound and validated once. A promise returned at runtime is ERR_FUNCTION_ASYNC, and its later rejection is logged; a controller with no create, an instance with no call or an async call is ERR_CONTROLLER_INVALID naming the function. What create allocates through ctx.effect is released at teardown and reload. The analyzer exports callableBodyField and parameterSchemaOf, which the kernel uses to tell a native function from one written in CEL; callArgumentBinding gains name, and bindNamed leaves an omitted required parameter absent for validation to report.

A native function can be written in Rust. The Node kernel loads a pkg:cargo function built with the Rust SDK's #[function(entry = "…")]: its entry namespace exports createFunction, the instance is created as an effect of the resource's create — so teardown and reload destroy it and its Drop runs then — and every call crosses as typed frames, so a timestamp written at +02:00 compares with a later Z one as an instant and bytes arrive byte for byte. A panic fails the calling expression as ERR_FUNCTION_FAILED carrying ERR_CONTROLLER_PANIC. A configuration its Config type cannot read fails creation with ERR_FUNCTION_CONFIG_INVALID; an entry that is not ASCII letters, digits and underscores is a compile error naming the attribute; and a panic in Drop for an instance the garbage collector releases undestroyed is written to stderr rather than aborting the process. — analyzer, kernel

The Rust controller ABI moves to version 3, which adds the function vtable: a prebuilt pkg:telo/local/dylib candidate states abi=telo-3, and a dylib built against version 2 is refused when the Rust kernel opens it. CONTROLLER_DYLIB_ABI_MISSING and the abi axis examples name telo-3.

decideValue(ctx, invokeCtx, path, kind, compute) is the single seam every durable decision point goes through — the step engine's if / while / switch / value, and a composer that drives its own body and therefore evaluates its collection or its per-turn condition before the engine ever sees a step list. Outside a durable run, and inside a collapsed region, it is compute() and nothing else. — sdk

assertJournalable now refuses everything the typed frame refuses, not only what JSON.stringify throws on. A class instance offering a toJSON stringified happily and replayed as a plain object — a value of a different type, with nothing reported anywhere — and is now ERR_DURABLE_UNJOURNALABLE_VALUE, carrying data.valuePath, the JSON Pointer of the offending node inside the value, beside the step path in data.path. A root undefined is not a value and is not refused: a step that produced nothing is journaled as an entry carrying no value. It returns the frame text it produced, so a backend records that rather than encoding the value a second time.

The recorded-value codec every backend follows is in the SDK beside the typed frame: RECORDED_VALUE_CODEC_VERSION, writeRecordedValue(value, where) (absent object members dropped, then assertJournalable), withoutAbsentMembers and readRecordedValue(run, path, version, written) — a value with no version read as its store read it before the codec, an unknown version refused with ERR_DURABLE_ENTRY_UNDECODABLE, and a frame that does not decode reported as ERR_DURABLE_JOURNAL_CORRUPT naming the run and path.

An invoke step's when: guard is journaled inside a durable run (<step>/@when, decision predicate — GUARD_DECISION_SEGMENT, a segment no step name can spell, so a step named when in the body the guarded step dispatches is not handed the guard's value), as a try: step's already was: re-derived on a resume, a guard reading a clock or observed state could turn false for a step whose effect had already happened, skipping it and leaving steps.<name> unset.

encodeTypedFrame / decodeTypedFrame write and read the typed frame: one schema-independent JSON form covering the whole CEL value domain, so a value read back has the CEL type it was written with. An int, uint, bytes, timestamp or duration is written as {"$telo":"<CEL type>","value":"<plain encoding>"}, as is a double that JSON cannot carry (NaN, ±Infinity, negative zero); a map with a non-string key or the key $telo is a tagged list of pairs; everything else is plain JSON, and an untagged number reads as a double. The text is canonical (RFC 8785 member order and number form), so identical values produce identical bytes. A value outside the domain — a class instance, including one with toJSON, a function, a symbol, undefined, a Map with a key CEL cannot hold or with two keys CEL reads as one (an int and a uint of the same number), a list carrying a property beside its items, a cycle — throws ERR_TYPED_FRAME_UNENCODABLE; a malformed frame, an unknown tag, a non-canonical payload, an object repeating a member name, or a number beyond a double's range (1e400) throws ERR_TYPED_FRAME_UNDECODABLE. Both carry data.path, the JSON Pointer of the offending node, a key's unpaired surrogates written as U+FFFD. TYPED_FRAME_TAGS lists the closed tag vocabulary. The format and its conformance vectors are specified in kernel/specs/durable-execution.md §6. — sdk


Changelogs: cli · analyzer · ide-support · kernel · sdk · templating