Projects in code
load() reads a project once and compiles it. A project is the owned files under a root, the reflexes handed as
code, and who answers. load() never fetches and never writes.
load(options)
const project = await load<Reflexes>({ root, reflexes, adapter })
| Option | Meaning |
|---|---|
root | The project directory: evoke.toml, evoke.lock, overlays/, vocab/, local reflexes. Absent: no files at all. A root without evoke.toml is the default project, adapter = "jev", nothing installed |
reflexes | Reflexes handed as code, by local name. A name evoke.toml also lists is an error. Overlays under root apply to them too |
adapter | Who answers. Absent: the adapter evoke.toml names, resolved from the SDK's own subpath and built under [adapters.<name>]. Required when there is no root |
platform | "linux" or "macos": the platform a manifest's platforms is judged by, this machine's when absent. A reflex named for another platform is inactive; name one to decide or test as that machine would. A body still runs here |
Remote reflexes come from the store, by the lock's content hash, and are hashed again at load. A missing entry
fails, and the fix is evoke sync. The SDK never checks trust, since the root is yours to choose. It keeps no cache.
Reflexes as code
const timer = reflex({
description: "Start a countdown timer.",
effect: "write", // absent means destructive
confirm: "Start a {duration} timer?", // required
args: { duration: { ask: "How long?", pick: "duration" } },
examples: { "timer for 10 minutes": { duration: "10 minutes" } },
}, async ({ duration }, { input, signal }) => startTimer(duration))
The manifest is the file's shape without run and config, yields and returns included. A body closes over
what it needs. Its types are inferred from the literal: an option key union, a word or a quoted pick as string,
a number or a duration as number, a flag as true, an optional argument as optional, and an argument written
{ takes: "deploys" } as unknown, an earlier step's whole result the weave hands the body. So the body's args
are typed with nothing generated. A manifest that does not read throws at load, since it is your own code. Its problems end in
reflex(<name>). The body runs in-process. It is your application's code, unscrubbed, and nothing can end it from
outside. So it honours the deadline's signal.
playbook(manifest) hands a playbook the same way: steps in the manifest and no body
(Playbooks). Its steps are decided over the project's reflexes when a sentence reaches
it, and it reports runs: "plan".
Generated types
evoke add, update and remove write evoke.d.ts next to evoke.toml. It holds one Reflexes interface,
with a member per installed reflex, and its arguments as a decision carries them. Hand it to load:
import type { Reflexes } from "./evoke.d.ts"
const project = await load<Reflexes>({ root: import.meta.dirname })
Now d.reflex narrows d.args and d.values. A project with reflexes both installed and handed as code writes
load<Reflexes & ReflexesOf<typeof own>>({ root, reflexes: own }). TypeScript infers all type arguments or none.
What a project knows
project.reflexes // per name: { active: true, effect, runs: "inline" | "file" | "argv" | "plan" } | { active: false, problems }
project.plan // the digest of the compiled set, "h1:…"; every decision carries it
An inactive reflex is left out of every decision. The causes are an empty vocabulary, a setting unset, or an
overlay that does not parse. Its problems each end in the command that fixes it.
with({ vocab }): a tenant's words
A vocabulary binds a project, not a call. with compiles the same set over vocabularies of its own, in the
file's form. It is synchronous and takes milliseconds:
const tenant = project.with({ vocab: { rooms: await db.rooms(user) } })
const d = await tenant.decide(input)
if (d.outcome === "run") await tenant.run(d)
decide, fill and run on that project cannot disagree about the plan the decision was made under. So a
multi-tenant server stays correct. A decision made on one tenant's project is refused by another's.
Running bodies from the SDK
A reflex the project names, by a git ref or a directory, runs in a child process, through the SDK's own loader,
under the SDK's Node, held to what its manifest declares under [needs] as far as the package can: under Node's
permission model on both systems, and under Seatbelt on macOS. An argv reflex is spawned directly, under the same
layers. On Linux the kernel's layer needs native code the package does not carry, so a file body runs partly held
and an argv body unheld there, and the result's contained says which, in the shape
--json prints. A reach past the declaration ends the run with what was
reached and the key, as it does in the CLI. Nothing starts before the decision. The body's deadline is 30
seconds, less what the adapter call took.
Next: Decisions.