Skip to content

ADR 0002: Execute ASTs with isolated async contexts

  • Status: Accepted
  • Date: 2026-07-21
  • Amended: 2026-07-25

Context

AST instances may be cached and evaluated concurrently. Mutating value nodes to inject callable or lambda arguments caused shared-state races and made repeated execution order-dependent.

Decision

Evaluation is asynchronous and uses an immutable per-invocation ExecutionContext. Named bindings resolve lambda parameters; identity bindings resolve compiled-tool parameters without relying on globally unique leaf names. Evaluation never mutates the AST to pass runtime values.

A single-mode program contains one composed root operation. Parallel mode evaluates independent body nodes concurrently. Function parameters are evaluated before one provider call. Conditionals evaluate only the selected branch. Map and filter lambdas have one parameter; reduce lambdas have accumulator and item parameters. A null reduce accumulator starts from the first item, while a non-null value is an explicit initializer. Empty reductions return None.

Cancellation propagates. Configured model deadlines raise TimeoutError. Provider, tool, validation, compilation, and execution failures use the public exception hierarchy where a domain-specific boundary exists.

Execution can receive immutable ExecutionLimits. Omitted fields are unlimited to preserve compatibility. A fresh shared budget is created for every direct or compiled invocation and counts:

  • every dynamic node evaluation, including repeated lambda bodies;
  • one-based structural evaluation depth;
  • provider tool invocations, excluding metadata lookup;
  • concurrently evaluated sibling operations; and
  • elapsed wall-clock duration.

Configured maxima are inclusive. Crossing a maximum raises ExecutionLimitError, including wall_clock_seconds when Treelang's deadline expires. A provider-raised TimeoutError remains a provider error, and external cancellation is never translated. Separate invocations never share counters. When concurrency is bounded, nested sibling operations inside an active budget slot execute sequentially to prevent permit deadlock.

Version 2 additionally limits active user-function frames with max_call_depth. This is independent of structural max_depth: repeated evaluation of the same declared body retains its static program-relative depth, while each recursive call increases call depth.

Consequences

  • The same tree can be invoked concurrently without corrupting later calls.
  • Lambda placeholder names are semantic and validated before execution.
  • Conditional branches are lazy, while function parameters and parallel program bodies may execute concurrently.
  • Generated or untrusted trees can be constrained without changing their schema.
  • Resource limits are runtime policy and are not serialized into version 1 ASTs.
  • Future execution optimizations must preserve these observable semantics.

Retries are opt-in and only apply to tools explicitly declared idempotent by the caller. Arguments are evaluated once, every physical attempt consumes the tool-call budget, retry backoff is cancellable, and cancellation itself is never retried. The default parallel failure mode cancels and awaits unfinished siblings before raising. An opt-in collection mode returns ordered success/failure outcomes and is rejected for single-mode programs.

Deterministic replay fixtures are ordered and validate complete model requests or tool arguments. Drift and unconsumed entries are errors. Fixture persistence is an application concern because request and response content may be sensitive.