Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 })
OptionMeaning
rootThe 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
reflexesReflexes handed as code, by local name. A name evoke.toml also lists is an error. Overlays under root apply to them too
adapterWho 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.

FAQPrivacyGitHub