Learn · from docs/services.md in the eLucid8 repository

Contents

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:

The services

ServiceOne lineAvailable in
sum_serviceStand-in: the sum of its numeric inputs, after the latency set in the Program paneEvery program
anomaly_detectorScores a reading against its neighbourhood mean, |reading − mean| roundedThe Spatial anomaly example

sum_service

The playground's stand-in, for trying deadlines and effects in programs you write.

InputsAny arguments. Numbers are added (inside lists too); anything else, including true/false and text, is ignored.
OutputA number: the sum of the numeric inputs (0 if there are none).
Error codesNone: it always answers. Only a missed deadline makes its outcome an Error (late).
TimingAnswers the time set in External service latency (ms) after the call starts (default 5 ms).
IdempotentYes (it never fails, so retries never happen).
Available inEvery 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.

InputsExactly 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.
OutputA whole number: |reading − mean|, rounded.
Error codesinvalid_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.
Timing4 + ((7x + 13y + 5·time) mod 23) ms after each attempt starts, so 4–26 ms. The External service latency box does not apply.
IdempotentYes: a failed call may be retried.
Available inThe 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:

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