#External services in the eLucid8 playground
An external service is a program outside eLucid8 that answers on request: an AI model, a web API, a simulator. A program calls one with an effect:
effect score @ (t) = call service_name(arg1, arg2, …) deadline 20ms retry overloaded up to 2 reuse successes
A source is external data you read; an effect calls an external service that computes an answer. The rules for effects are in the semantics, §5 and the tutorial, section 10.
The playground supplies the services below. It lists the ones a program can call in its Program pane. A call to any other name is a compile error, for example "unknown service: sum_servce; available: sum_service", so a misspelt name never returns a plausible number.
The examples on this page are checked against the playground engine by packages/evaluator/test/services.test.ts.
#What every service call does
These come from the evaluator, whatever the service:
- Arguments: the call's arguments are evaluated at the effect's context. If any of them is Undefined or an Error, the service is not called, and the effect's outcome is that Undefined or Error.
- Start time: on a clocked dimension a call starts at its tick's timestamp (C5); otherwise at 0 ms. The
deadlinecounts from the first attempt's start. - Outcomes: an answer is a value. A failure is an Error, shown as
service error: <code>. An answer or failure after the deadline islate (<finished> ms > deadline <deadline> ms). - Retries:
retry <codes> up to nallows at most n attempts in total, only for the listed error codes, only before the deadline, and only for an idempotent service. Each retry starts when the previous attempt ends. n must be from 1 to 10; any other is a compile error. - Answers: an answer is used as it is. A non-finite number answered (for example
sum_serviceadding a huge number to itself) is a value, as source data is; an Error arises only when an operator then produces a non-finite result. - Once per query: a call is made once per query and context, never speculatively (for example for the other points of a tile).
- Across queries: a successful answer is reused by later queries only with
reuse successes, through the point cache; failures are never reused. Without it, running the query again calls the service again.
#The services
| Service | One line | Available in |
|---|---|---|
sum_service | Stand-in: the sum of its numeric inputs, after the latency set in the Program pane | Every program |
anomaly_detector | Scores a reading against its neighbourhood mean, |reading − mean| rounded | The Spatial anomaly example |
#sum_service
The playground's stand-in, for trying deadlines and effects in programs you write.
| Inputs | Any arguments. Numbers are added (inside lists too); anything else, including true/false and text, is ignored. |
| Output | A number: the sum of the numeric inputs (0 if there are none). |
| Error codes | None: it always answers. Only a missed deadline makes its outcome an Error (late). |
| Timing | Answers the time set in External service latency (ms) after the call starts (default 5 ms). |
| Idempotent | Yes (it never fails, so retries never happen). |
| Available in | Every program in the playground. |
-- sum_service on time and late.
dimension time : int clock sensor
source x @ (time)
effect total @ (time) = call sum_service(x, 1) deadline 20ms
-- clock sensor = 0, 100, 200
-- data x = #time
-- latency 5
-- expect total {time = 2} = 3
With the latency set to 30 ms, the same call finishes at 230 ms, after its 220 ms deadline:
-- sum_service late.
dimension time : int clock sensor
source x @ (time)
effect total @ (time) = call sum_service(x, 1) deadline 20ms
-- clock sensor = 0, 100, 200
-- data x = #time
-- latency 30
-- expect total {time = 2} = Error: late (230 ms > deadline 220 ms)
#anomaly_detector
The Spatial anomaly example's detector (deterministicDetector in packages/evaluator/demo/workload.ts): deterministic, with timings and failures that depend on the point, so that some calls are late and some need a retry.
| Inputs | Exactly two numbers, a reading and its neighbourhood mean, in that order. Nothing is converted: text, true/false and lists are not numbers. The calling effect must have the integer dimensions time, x and y. |
| Output | A whole number: |reading − mean|, rounded. |
| Error codes | invalid_input: the inputs are not exactly two numbers, or the calling effect lacks an integer time, x or y (a missing or text-label coordinate). It fails as the attempt starts, on every attempt, so a retry never helps. overloaded: on the first attempt only, at points where x + y + time is a multiple of 11. It fails 3 ms after the attempt starts, and a retry succeeds. |
| Timing | 4 + ((7x + 13y + 5·time) mod 23) ms after each attempt starts, so 4–26 ms. The External service latency box does not apply. |
| Idempotent | Yes: a failed call may be retried. |
| Available in | The Spatial anomaly example (README workload), including programs you write after choosing it. |
-- anomaly_detector: a hot spot, a retry, a failure without retry, and a late answer.
dimension time : int clock sensor
dimension x : int range 0..15
dimension y : int range 0..15
source field @ (time, x, y)
smooth @ (time, x, y) = mean(neighbourhood(field, x in -1..1, y in -1..1, boundary truncate))
effect score @ (time, x, y) = call anomaly_detector(field, smooth) deadline 20ms retry overloaded up to 2
effect once @ (time, x, y) = call anomaly_detector(field, smooth) deadline 20ms
-- clock sensor = 0, 100, 200, 300
-- data field = if #x == 5 and #y == 5 then 50 else 10
-- expect score {time = 1, x = 5, y = 6} = 4
-- expect score {time = 1, x = 5, y = 5} = 36
-- expect once {time = 1, x = 5, y = 5} = Error: service error: overloaded
-- expect score {time = 2, x = 4, y = 4} = Error: late (225 ms > deadline 220 ms)
How to read it:
- At (time 1, x 5, y 6) the reading is 10 and its 3×3 neighbourhood includes the hot spot: (50 + 8 × 10) / 9 ≈ 14.4, so the score is |10 − 14.4| rounded = 4. The call takes 7 ms, inside its deadline.
- At (time 1, x 5, y 5), x + y + time = 11, so the first attempt fails
overloadedat 103 ms.scoreretries: the second attempt starts at 103 ms and answers at 120 ms, exactly at the deadline (100 + 20 ms), with |50 − 14.4| rounded = 36.oncehas no retry clause, so its outcome is the failure. - At (time 2, x 4, y 4) the call takes 25 ms and finishes at 225 ms, after its 220 ms deadline:
late.
The detector is strict about its inputs: anything other than two numbers fails invalid_input (owner decision, 2026-10-04; before, text, true/false and lists were converted as JavaScript does):
-- anomaly_detector: inputs that are not two numbers.
dimension time : int clock sensor
dimension x : int range 0..15
dimension y : int range 0..15
source field @ (time, x, y)
effect flagged @ (time, x, y) = call anomaly_detector(field > 20, field) deadline 20ms
effect single @ (time, x, y) = call anomaly_detector(field) deadline 20ms
effect row @ (time, x) = call anomaly_detector(field @ {y = 0}, 1) deadline 20ms
-- clock sensor = 0, 100
-- data field = 10
-- expect flagged {time = 1, x = 2, y = 3} = Error: service error: invalid_input
-- expect single {time = 1, x = 2, y = 3} = Error: service error: invalid_input
-- expect row {time = 1, x = 2} = Error: service error: invalid_input
row has no y, so the detector fails it too, whatever its inputs.
#Outside the playground
The Rust runtime (the elucid8 command) has the same two services built in, with the same answers, timings and failures, and calls any other service through a small out-of-process protocol: service-protocol.md.
#Proposed later
servicedeclarations in the language, so that calls are checked by the compiler itself, not only by the playground.- A user-defined pretend service: a formula over its inputs, a latency and an injected failure pattern, to try deadlines and retries.