This is the full developer documentation for Skein # Skein Language > A new programming language that compiles to BEAM bytecode, designed for building cloud services where AI agents are first-class constructs. ## What is Skein? [Section titled “What is Skein?”](#what-is-skein) Skein is a programming language that compiles to BEAM bytecode and runs on the Erlang VM (OTP). It is co-optimized for two audiences: **humans who read and review code**, and **LLM agents who generate and modify code**. The language sits at the intersection of three ideas: 1. **The BEAM/OTP runtime** provides battle-tested concurrency, fault tolerance, and distribution — exactly what long-running agent processes need. 2. **An integrated platform model** with trace-driven development and the “service as the unit of work” philosophy. 3. **Agent-writability as a first-class design constraint** — a language whose entire spec fits in an LLM context window, with maximally regular syntax and a type system that doubles as a contract language for tool calling. ## Current Status [Section titled “Current Status”](#current-status) The full compilation pipeline is operational. You can write `.skein` files, compile them to BEAM bytecode, and run them — including making real LLM calls to Claude with full tracing. **What works today:** * **A compact construct set** — modules, functions, types, enums, agents, tools, supervisors, handlers (HTTP/queue/schedule), tests (plus `scenario` and `golden` trace tests), pattern matching, pipes, capabilities * **Full type system** — type checking, type inference (field access resolves types), JSON schema derivation, constraint annotations * **Capability enforcement** — compile-time and runtime checking for both modules and agents across all subsystems (HTTP, store, memory, LLM, tools) * **Production LLM backend** — Anthropic integration with `llm.chat`, `llm.json`, and `llm.stream`, with token-level usage tracking * **Rich traces** — every effect call produces structured trace spans with timing, outcome, and token usage * **Agent state machines** — `gen_statem`-based agents with compile-time phase transition verification * **CLI tooling** — `skein new`, `compile`, `build`, `test`, `run`, and `trace`, plus `agents` (AGENTS.md scaffolding), `mcp` (MCP server for coding agents), `lsp`, `completions`, `version`, and `help` * **Editor support** — VS Code extension with syntax highlighting, snippets, and full LSP (diagnostics, hover, go-to-definition, completions) An extensive suite of unit, integration, and property-based tests passes clean across all four apps (compiler, runtime, CLI, LSP). ## Quick Example [Section titled “Quick Example”](#quick-example) ```skein module Hello { fn greet(name: String) -> String { "Hello, ${name}!" } fn add(a: Int, b: Int) -> Int { a + b } fn classify(n: Int) -> String { match n > 0 { true -> "positive" false -> "non-positive" } } } ``` Compile and run — no Erlang or Elixir install required: ```bash skein compile hello.skein # → Compiled: Skein.User.Hello ``` Scaffold a full project with tests, build everything, and start a server: ```bash skein new my-service cd my-service skein build skein test skein run ``` ## LLM Demo — Real Claude Calls with Traces [Section titled “LLM Demo — Real Claude Calls with Traces”](#llm-demo--real-claude-calls-with-traces) Skein modules can declare LLM capabilities and make type-checked, traced calls to Claude: ```skein module Demo { capability model("anthropic", "claude-opus-4-8") fn greet(name: String) -> String { llm.chat("claude-opus-4-8", "You are friendly.", "Say hello to ${name} in one sentence.")! } fn classify(text: String) -> String { llm.chat("claude-opus-4-8", "Answer with one word.", "Classify this as positive or negative: ${text}")! } } ``` ```bash skein compile demo.skein # ...exercise the module (e.g. via skein test or a running service), then: skein trace --last 2 --kind llm ``` ```plaintext Traces (2): [llm] chat (1244ms) [llm] chat (387ms) ``` Every LLM call is capability-gated (the module must declare `capability model(...)`), type-checked, and automatically traced with token usage. # Code Generator > How the Skein code generator produces BEAM bytecode via Core Erlang. ## Overview [Section titled “Overview”](#overview) The code generator (`Skein.CodeGen.CoreErlang`) translates the Skein AST into Core Erlang using Erlang’s `:cerl` module, then compiles to BEAM bytecode via `:compile.forms/2`. **Location:** `apps/skein_compiler/lib/skein/codegen/core_erlang.ex` — the largest compiler stage alongside the parser ## What is Core Erlang? [Section titled “What is Core Erlang?”](#what-is-core-erlang) Core Erlang is a simplified, explicit intermediate representation for the BEAM VM. It’s what Elixir, LFE, Gleam, and now Skein compile to. Key properties: * All variables are single-assignment * All functions are explicitly named with arity * Pattern matching is lowered to `case` expressions * No syntactic sugar — everything is explicit * The `:cerl` module provides an API for constructing Core Erlang AST nodes programmatically ## The `:cerl` Module [Section titled “The :cerl Module”](#the-cerl-module) Instead of generating Core Erlang text, the code generator builds AST nodes using `:cerl`: ```elixir # Create a module :cerl.c_module(name, exports, attributes, definitions) # Create a function :cerl.c_fun(params, body) # Create a case expression :cerl.c_case(subject, clauses) # Create a let binding :cerl.c_let(vars, value, body) # Create a function call (to another module) :cerl.c_call(module, function, args) # Create a local function application :cerl.c_apply(function, args) # Create literals :cerl.abstract(42) # integer :cerl.abstract("hello") # binary string :cerl.abstract(true) # boolean atom # Create variables :cerl.c_var(:MyVar) # Create a list :cerl.make_list(elements) ``` ## Module Generation [Section titled “Module Generation”](#module-generation) A Skein module compiles to a Core Erlang module with: 1. The module name atom (`Elixir.Skein.User.`) 2. An export list of all functions + `__info__/1` 3. Function definitions 4. An `__info__/1` function for Elixir compatibility ```elixir def generate(%AST.Module{} = ast) do module_atom = String.to_atom("Elixir.Skein.User.#{ast.name}") fns = Enum.filter(ast.declarations, &match?(%AST.Fn{}, &1)) exports = Enum.map(fns, fn f -> :cerl.c_fname(String.to_atom(f.name), length(f.params)) end) defs = Enum.map(fns, fn f -> fname = :cerl.c_fname(String.to_atom(f.name), length(f.params)) fun = generate_fn(f) {fname, fun} end) # Add __info__/1 info_fname = :cerl.c_fname(:__info__, 1) info_fun = generate_info_fn(module_atom, fns) mod = :cerl.c_module( :cerl.c_atom(module_atom), [info_fname | exports], [], [{info_fname, info_fun} | defs] ) case :compile.forms(mod, [:from_core, :binary, :return_errors]) do {:ok, _, beam_binary} -> {:ok, [{module_atom, beam_binary}]} {:ok, _, beam_binary, _warnings} -> {:ok, [{module_atom, beam_binary}]} {:error, errors, _warnings} -> {:error, format_errors(errors)} end end ``` `generate/1` returns a list of **named binaries** — `{:ok, [{module, binary()}]}` — because one source file can produce several BEAM modules: a module with nested agents yields the module itself (`Skein.User.Foo`) followed by one entry per agent (`Skein.Agent.Foo.Bar`). Nested agents are generated with the module’s capabilities and type declarations in scope, so `llm.json[T]` schema resolution works inside agent handlers. The compiler driver loads every entry and returns the first (primary) module. ## Function Generation [Section titled “Function Generation”](#function-generation) Each Skein function becomes a Core Erlang function: ```skein fn add(a: Int, b: Int) -> Int { a + b } ``` Generates: ```erlang 'add'/2 = fun (A, B) -> call 'erlang':'+'(A, B) ``` The generator: 1. Creates parameter variables (snake\_case -> CamelCase) 2. Builds a scope map from parameter names to Core Erlang variables 3. Recursively generates the body expression 4. Wraps in `:cerl.c_fun/2` ## Expression Generation [Section titled “Expression Generation”](#expression-generation) ### Literals [Section titled “Literals”](#literals) | Skein | Core Erlang via :cerl | | --------- | ------------------------- | | `42` | `:cerl.abstract(42)` | | `3.14` | `:cerl.abstract(3.14)` | | `true` | `:cerl.abstract(true)` | | `false` | `:cerl.abstract(false)` | | `"hello"` | `:cerl.abstract("hello")` | ### String Interpolation [Section titled “String Interpolation”](#string-interpolation) `"Hello, ${name}!"` compiles to an iolist passed to `erlang:iolist_to_binary/1`: ```elixir # Build the iolist: ["Hello, ", Name, "!"] parts = [ :cerl.abstract("Hello, "), :cerl.c_var(:Name), :cerl.abstract("!") ] iolist = :cerl.make_list(parts) # Wrap in erlang:iolist_to_binary/1 :cerl.c_call( :cerl.c_atom(:erlang), :cerl.c_atom(:iolist_to_binary), [iolist] ) ``` This approach was chosen over binary construction (`<<>>`) because: * It handles mixed literal and dynamic segments naturally * The `:cerl.abstract/1` function produces proper binary literals * `iolist_to_binary` is efficient and well-understood on the BEAM ### Binary Operations [Section titled “Binary Operations”](#binary-operations) Binary ops compile to `erlang` BIF calls (division is special-cased; see the footnote below): ```elixir # a + b :cerl.c_call(:cerl.c_atom(:erlang), :cerl.c_atom(:+), [left, right]) ``` Operator mapping: | Skein | Erlang BIF | | ------ | ---------------------------------- | | `+` | `:+` | | `-` | `:-` | | `*` | `:*` | | `/` | `:div` / `:/` (runtime dispatch)\* | | `==` | `:==` | | `!=` | `:"/="` | | `<` | `:<` | | `>` | `:>` | | `<=` | `:"=<"` | | `>=` | `:>=` | | `&&` | `:and` | | `\|\|` | `:or` | \* Division does not compile to a single BIF call. The generator binds both operands, then emits a `case` on `erlang:is_float/1` of either operand: if either is a float, it calls `erlang:'/'` (float division); otherwise it calls `erlang:div` (integer division). So `7 / 2` is `3` while `7.0 / 2` is `3.5`. ### Let Bindings [Section titled “Let Bindings”](#let-bindings) ```skein let x = expr body ``` Compiles to: ```elixir :cerl.c_let( [:cerl.c_var(:X)], # bound variable generate_expr(value), # value expression generate_expr(body) # body where variable is in scope ) ``` ### Match Expressions [Section titled “Match Expressions”](#match-expressions) ```skein match subject { true -> "yes" false -> "no" } ``` Compiles to a Core Erlang `case`: ```elixir :cerl.c_case( generate_expr(subject), [ :cerl.c_clause([:cerl.abstract(true)], :cerl.abstract("yes")), :cerl.c_clause([:cerl.abstract(false)], :cerl.abstract("no")) ] ) ``` ### Function Calls [Section titled “Function Calls”](#function-calls) Local function calls use `:cerl.c_apply/2`: ```elixir :cerl.c_apply( :cerl.c_fname(String.to_atom(name), arity), args ) ``` ### Blocks [Section titled “Blocks”](#blocks) Multi-expression blocks use chained `let` bindings. Each expression except the last is bound to a unique temporary variable: ```skein { let x = a + b let y = x * 2 y } ``` The last expression becomes the body of the innermost `let`. ## Enum Variant Construction [Section titled “Enum Variant Construction”](#enum-variant-construction) Variant constructors lower to the same shapes the pattern side matches: | Skein expression | Core Erlang value | | ------------------------------------------------- | -------------------------- | | `Status.Active`, bare `Active`, `Status.Active()` | `:active` (bare atom) | | `Status.Banned("spam")`, bare `Banned("spam")` | `{:banned, "spam"}` | | `Ok(x)` / `Err(e)` | `{:ok, X}` / `{:error, E}` | | `SearchError.from(e)` | `{:search_error, E}` | Zero-field variants are **bare atoms**, not 1-tuples, so constructions round-trip through `match` arms (`Active -> ...` compiles to the atom pattern `:active`). Variant names are snake\_cased (`ChargeSucceeded` -> `:charge_succeeded`). The analyzer validates constructions before codegen runs: unknown variants (E0010, with a closest-name `fix_code`), wrong arity, wrong argument types, and a data variant referenced without arguments (E0020, with a `Enum.Variant(fields)` skeleton) are all structured compile errors — never `core_lint` crashes. ## Variable Naming [Section titled “Variable Naming”](#variable-naming) Skein snake\_case variables are converted to Core Erlang CamelCase: ```elixir defp var_name(name) when is_binary(name) do name |> String.split("_") |> Enum.map(&String.capitalize/1) |> Enum.join() |> String.to_atom() end ``` | Skein | Core Erlang | | ----------- | ----------- | | `x` | `X` | | `name` | `Name` | | `my_var` | `MyVar` | | `ticket_id` | `TicketId` | ## Temporary Variables [Section titled “Temporary Variables”](#temporary-variables) The generator needs unique variables for intermediate results. These are created with a process-local counter: ```elixir defp gen_var do counter = Process.get(:skein_var_counter, 0) Process.put(:skein_var_counter, counter + 1) String.to_atom("_skein_#{counter}") end ``` ## Compilation [Section titled “Compilation”](#compilation) The final step compiles the Core Erlang AST to BEAM bytecode: ```elixir :compile.forms(core_module, [:from_core, :binary, :return_errors]) ``` The `:from_core` flag tells the compiler the input is Core Erlang (not Erlang source). The `:binary` flag returns the bytecode as a binary rather than writing to disk. The `:return_errors` flag returns errors as data rather than printing to stderr. ## Stdlib Call Generation [Section titled “Stdlib Call Generation”](#stdlib-call-generation) When the code generator encounters a call to a known stdlib module like `String.upcase(s)`, it generates a remote call to the corresponding runtime module: ```skein -- Skein source: String.upcase(name) -- Compiles to (Core Erlang): call 'Elixir.Skein.Runtime.Stdlib.String':'upcase'(Name) ``` The mapping from Skein module names to Elixir runtime modules: | Skein Module | Runtime Module | | ------------ | ------------------------------- | | `String` | `Skein.Runtime.Stdlib.String` | | `Int` | `Skein.Runtime.Stdlib.Int` | | `Float` | `Skein.Runtime.Stdlib.Float` | | `List` | `Skein.Runtime.Stdlib.List` | | `Map` | `Skein.Runtime.Stdlib.Map` | | `Set` | `Skein.Runtime.Stdlib.Set` | | `Option` | `Skein.Runtime.Stdlib.Option` | | `Result` | `Skein.Runtime.Stdlib.Result` | | `Uuid` | `Skein.Runtime.Stdlib.Uuid` | | `Instant` | `Skein.Runtime.Stdlib.Instant` | | `Duration` | `Skein.Runtime.Stdlib.Duration` | No capabilities are required for stdlib calls. See the [Standard Library reference](/Skein/reference/stdlib/) for the full API. ## Effect Call Generation [Section titled “Effect Call Generation”](#effect-call-generation) When the code generator encounters an effect call like `http.get(url)`, it generates a remote call to the runtime instead of a local apply: ```skein -- Skein source: http.get(url) -- Compiles to (Core Erlang): call 'Elixir.Skein.Runtime.Http':'get'(Url, Capabilities) ``` The generator recognizes effect calls by pattern-matching on `Call{target: FieldAccess{subject: Identifier{name}, field}}` where `name` is a known effect namespace. ### Capabilities parameter [Section titled “Capabilities parameter”](#capabilities-parameter) Every effect call appends the module’s compiled capabilities list as the final argument. This is built at compile time from the module’s `capability` declarations and enables runtime enforcement: ```skein -- Skein source (module has: capability http.out("api.example.com")) http.get(url) -- Compiles to: call 'Elixir.Skein.Runtime.Http':'get'( Url, [#{kind => "http.out", params => ["api.example.com"]}] ) ``` ### Effect namespace to runtime module mapping [Section titled “Effect namespace to runtime module mapping”](#effect-namespace-to-runtime-module-mapping) | Namespace | Runtime Module | Operations | | --------------- | -------------------------- | --------------------------------------- | | `http` | `Skein.Runtime.Http` | `get`, `post`, `put`, `patch`, `delete` | | `store.` | `Skein.Runtime.Store` | `get`, `put`, `delete`, `query` | | `memory` | `Skein.Runtime.Memory` | `put`, `get`, `delete`, `list` | | `llm` | `Skein.Runtime.Llm` | `chat`, `json`, `stream`, `embed` | | `tool` | `Skein.Runtime.Tool` | `call`, `list`, `schema` | | `topic` | `Skein.Runtime.Topic` | `publish` | | `queue` | `Skein.Runtime.Queue` | `publish` | | `trace` | `Skein.Runtime.Trace` | `annotate` | | `event` | `Skein.Runtime.EventStore` | `log` | | `process` | `Skein.Runtime.Process` | `spawn` | | `timer` | `Skein.Runtime.Timer` | `after`, `interval`, `cancel` | ### HTTP effect compilation [Section titled “HTTP effect compilation”](#http-effect-compilation) HTTP calls follow the generic pattern — the namespace maps to `Skein.Runtime.Http` and the method becomes the function name: ```skein http.post("https://api.example.com/data", body) -- Compiles to: call 'Elixir.Skein.Runtime.Http':'post'(Url, Body, Capabilities) ``` ### Store effect compilation [Section titled “Store effect compilation”](#store-effect-compilation) Store effects have a three-level namespace: `store.
.`. The code generator extracts the table name and passes it as the first argument: ```skein store.users.get(id) -- Compiles to: call 'Elixir.Skein.Runtime.Store':'get'("users", Id, Capabilities) ``` For typed tables (C5), `put` carries two extra arguments immediately before the capabilities: the table’s derived JSON Schema, so the runtime schema-checks every write (defense in depth behind the analyzer’s record typing, and a guard for dynamic data that reached the record through `Json` seams), and the record type’s `@primary` field name (#340), so rows are keyed by the declared primary rather than a hard-coded `id` — `call 'Elixir.Skein.Runtime.Store':'put'("users", Record, Schema, Primary, Capabilities)`. ### Memory effect compilation [Section titled “Memory effect compilation”](#memory-effect-compilation) Memory effects extract the namespace from the `memory.kv(namespace)` capability declaration and pass it as the first argument: ```skein -- Module has: capability memory.kv("sessions") memory.put("user", user_id) -- Compiles to: call 'Elixir.Skein.Runtime.Memory':'put'("sessions", "user", UserId, Capabilities) ``` ### LLM effect compilation [Section titled “LLM effect compilation”](#llm-effect-compilation) LLM calls have special handling for each method: **`llm.chat`** — standard text response: ```skein llm.chat("claude-opus-4-8", "System prompt", input) -- Compiles to: call 'Elixir.Skein.Runtime.Llm':'chat'("claude-opus-4-8", "System prompt", Input, Capabilities) ``` **`llm.json[T]`** — schema-constrained JSON. The type parameter is compiled into a JSON Schema literal using `SchemaGen`: ```skein llm.json[Decision]("claude-opus-4-8", "Decide action", input) -- Compiles to: call 'Elixir.Skein.Runtime.Llm':'json'("claude-opus-4-8", "Decide action", Input, Schema, Capabilities) ``` Where `Schema` is the JSON Schema derived from the `Decision` type definition at compile time. **`llm.stream`** — streaming response: ```skein llm.stream("claude-opus-4-8", "Generate report", data) -- Compiles to: call 'Elixir.Skein.Runtime.Llm':'stream'("claude-opus-4-8", "Generate report", Data, NoOpCallback, Capabilities) ``` **`llm.embed`** — embedding vector: ```skein llm.embed("voyage-3-large", input) -- Compiles to: call 'Elixir.Skein.Runtime.Llm':'embed'("voyage-3-large", Input, Capabilities) ``` ### Tool effect compilation [Section titled “Tool effect compilation”](#tool-effect-compilation) Tool calls lower tool identifier references to string literals: ```skein tool.call(Stripe.CreateRefund, { amount: 100 }) -- Compiles to: call 'Elixir.Skein.Runtime.Tool':'call'("Stripe.CreateRefund", Args, Capabilities) ``` ### Respond compilation [Section titled “Respond compilation”](#respond-compilation) Handler response helpers compile to tagged tuples (not runtime module calls): ```skein respond.json(200, data) -- {:respond_json, 200, data} respond.text(200, body) -- {:respond_text, 200, body} respond.html(200, page) -- {:respond_html, 200, page} ``` ### Idempotent compilation [Section titled “Idempotent compilation”](#idempotent-compilation) The `idempotent(key)` construct compiles to a runtime check: ```skein idempotent("process-order-123") -- Compiles to: call 'Elixir.Skein.Runtime.Idempotent':'check!'("process-order-123") ``` ### Generic effect compilation [Section titled “Generic effect compilation”](#generic-effect-compilation) Any effect namespace listed in the runtime modules map that doesn’t have special handling follows the generic pattern: ```skein namespace.method(arg1, arg2) -- Compiles to: call 'Elixir.Skein.Runtime.':'method'(Arg1, Arg2, Capabilities) ``` This covers `http.*`, `trace.annotate`, `queue.publish`, `topic.publish`, `uuid.new`, and `instant.now`. The namespace-to-module map is derived from the authoritative effect-ABI registry (`Skein.EffectABI`, #296) — the same registry that drives the analyzer’s effect tables and the spec §6 signature drift test. ### Scoped effect compilation [Section titled “Scoped effect compilation”](#scoped-effect-compilation) The scoped effects — `process.spawn`, `timer.after`, `timer.interval`, `timer.cancel`, and `event.log` — thread the declared capability scope label (the pool/group/stream name from the capability declaration, spec §3.2) into the runtime call as the first argument: ```skein -- Module has: capability process.spawn("workers") process.spawn(task) -- Compiles to: call 'Elixir.Skein.Runtime.Process':'spawn'("workers", Task, Capabilities) ``` A parameterless capability declaration leaves the label `nil` (unscoped — the runtime checks presence only). The first declared capability of the kind wins; nested agents list their own capabilities before the module’s, so an agent-level label overrides the module’s. ## Handler Generation [Section titled “Handler Generation”](#handler-generation) Handler declarations compile to regular functions with metadata. Each handler produces: 1. An index-based handler function (`__handler_0__/1`, `__handler_1__/1`, …) that takes a request map and returns a response 2. An entry in `__handlers__/0` naming the source, method, route pattern, and handler function ```skein -- Skein source: handler http GET "/users/:id" (req) -> { req.params.id } -- Generates: -- 1. A function __handler_0__/1 -- 2. Metadata in __handlers__/0: -- [%{source: :http, method: :get, route: "/users/:id", handler: :__handler_0__}] ``` Route parameters are not listed in the metadata — the router extracts them from the route pattern at dispatch time. `method` is a lowercase atom (`nil` for queue/topic/schedule handlers). ## `__capabilities__/0` Function [Section titled “\_\_capabilities\_\_/0 Function”](#__capabilities__0-function) Every compiled module includes a `__capabilities__/0` function that returns the module’s declared capabilities as a list of maps: ```elixir Skein.User.MyService.__capabilities__() #=> [%{kind: "http.out", params: ["api.example.com"]}] ``` A module with no capabilities returns an empty list. Each capability map has: * `:kind` — the capability type (e.g., `"http.out"`) * `:params` — list of string parameters (e.g., host allowlist) ## Property-Tested Invariants [Section titled “Property-Tested Invariants”](#property-tested-invariants) The code generator’s property-based tests verify invariants including: * Integer addition computes correctly for random inputs * Integer subtraction computes correctly for random inputs * Integer multiplication computes correctly for random inputs * Comparison `>` produces correct booleans * Equality `==` produces correct booleans * String interpolation round-trips alphanumeric input * Let bindings preserve computed values * Match on `> 0` correctly classifies positive vs non-positive * Plain string literals return exact strings # Structured Errors > How the Skein compiler reports errors in a machine-readable format. ## Error Design [Section titled “Error Design”](#error-design) Skein compiler errors are structured data, not just human-readable strings. Every error can serialize to JSON and includes fields designed for LLM consumption. This is a core feature of the language — when an AI agent generates Skein code that fails to compile, the structured error gives it enough information to understand and fix the issue automatically. ## Error Structure [Section titled “Error Structure”](#error-structure) ```elixir %Skein.Error{ code: "E0001", # Stable error code severity: :error, # :error or :warning message: "Unexpected token '}'", # Human-readable description location: %{ # Where in the source file: "hello.skein", line: 5, col: 12 }, context: nil, # Optional surrounding code context fix_hint: "Expected expression after 'let x ='", # What to do about it fix_code: nil, # Optional suggested code fix span: nil, # Exact extent of the problem (1-based, end-exclusive) edit_kind: nil # How to apply fix_code mechanically (nil = template) } ``` ## Fields [Section titled “Fields”](#fields) | Field | Type | Purpose | | ----------- | ------------- | ---------------------------------------------------------------------------------------------------- | | `code` | String | Stable identifier for the error category (e.g., `"E0001"`) | | `severity` | Atom | `:error` (compilation fails) or `:warning` (compilation proceeds) | | `message` | String | Human-readable description of the problem | | `location` | Map | Source file, line number, column number | | `context` | String or nil | The code around the error for display | | `fix_hint` | String or nil | Suggestion for how to fix the issue | | `fix_code` | String or nil | Exact code that would fix the issue | | `span` | Map or nil | `%{start: %{line, col}, end: %{line, col}}` — the exact source extent (1-based; `end.col` exclusive) | | `edit_kind` | Atom or nil | How to apply `fix_code` as a mechanical edit (see below) | ## Machine-Applicable Fixes [Section titled “Machine-Applicable Fixes”](#machine-applicable-fixes) When `fix_code` is an exact edit (not an illustrative template), the error also carries `span` and `edit_kind`, so any consumer — the LSP, `skein mcp` clients, agents — can apply the fix without per-error-code logic: | `edit_kind` | Application | | ---------------- | ---------------------------------------------------------------------------------- | | `:replace` | Replace the spanned text with `fix_code` (empty `fix_code` deletes it) | | `:insert_before` | Insert `fix_code` immediately before `span.start` | | `:insert_after` | Insert `fix_code` immediately after `span.end` | | `:insert_line` | Insert `fix_code` as a new line at `span.start.line`, indented to `span.start.col` | | `:delete_line` | Delete the line(s) from `span.start.line` through `span.end.line` | `Skein.Error.Edit.apply_fix/2` is the reference implementation of these semantics. A `nil` `edit_kind` means the `fix_code` (if any) is a template like `"fn name() -> Type { ... }"` — useful as a hint, not a verbatim edit. ## JSON Serialization [Section titled “JSON Serialization”](#json-serialization) Errors serialize to JSON for tool integration: ```json { "code": "W0001", "severity": "warning", "message": "Unused binding 'order'", "location": { "file": "hello.skein", "line": 5, "col": 3 }, "fix_hint": "Remove this binding or prefix with _ to indicate it is intentionally unused", "fix_code": "_order", "span": { "start": { "line": 5, "col": 7 }, "end": { "line": 5, "col": 12 } }, "edit_kind": "replace" } ``` ## Error Flow [Section titled “Error Flow”](#error-flow) Errors are returned as lists throughout the pipeline: ```elixir # Lexer error {:error, [%Skein.Error{code: "E0001", message: "Unexpected character: #"}]} # Parser error {:error, [%Skein.Error{code: "E0001", message: "Expected 'module', got 'ident'"}]} # Code generator error {:error, [%Skein.Error{message: "Core Erlang compilation failed: ..."}]} ``` The compiler’s `with` chain short-circuits on the first error list. ## Error Code Reference [Section titled “Error Code Reference”](#error-code-reference) All error codes are aligned with the language specification. Agents can rely on stable codes for automated error handling. ### Syntax Errors [Section titled “Syntax Errors”](#syntax-errors) | Code | Severity | Description | | ----- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | E0001 | error | Unexpected token | | E0002 | error | Invalid string: unterminated literal, expression inside `${...}` interpolation, empty interpolation (`${}`), or unterminated interpolation | | E0003 | error | Invalid number literal (e.g. underscore grouping in a float: `1_000.5`) | ### Name Resolution Errors [Section titled “Name Resolution Errors”](#name-resolution-errors) | Code | Severity | Description | | ----- | -------- | ------------------------------------------------------------------------------------------------------ | | E0010 | error | Undefined identifier, unknown `&fn` reference, call to an undeclared fn, or unknown store-table method | | E0011 | error | Duplicate definition | | E0016 | error | Cross-module function call (functions are module-private; expose a tool instead) | ### Capability Errors [Section titled “Capability Errors”](#capability-errors) | Code | Severity | Description | | ----- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | E0012 | error | Missing capability for effect call | | E0013 | reserved | Capability parameter mismatch | | E0014 | error | Tool name not declared | | E0015 | error | Duplicate tool short name | | E0017 | error | Duplicate scoped capability declaration (`memory.kv`, `event.log`, `process.spawn`, `timer` allow one per module or agent) | | E0028 | error | Scenario capability envelope missing/incomplete: a tool the scenario calls has no `capability tool.use(T)` envelope, or the envelope does not cover the tool’s transitive effect summary | | E0029 | error | Effect in a pure context: a `test` body or a scenario `implement` provider block reaches a capability-gated effect, directly or transitively through local fn calls/`&fn` references — effects belong in `scenario`, and providers must be pure | Example: ```json { "code": "E0012", "severity": "error", "message": "Capability 'http.out' required but not declared. Effect calls to 'http' require this capability.", "location": {"file": "service.skein", "line": 5, "col": 5}, "fix_hint": "Add a capability declaration to the module: capability http.out", "fix_code": "capability http.out" } ``` The `fix_code` field is especially useful for LLM agents — it provides the exact text to insert to resolve the error. ### Type Errors [Section titled “Type Errors”](#type-errors) | Code | Severity | Description | | ----- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | E0020 | error | Type mismatch (including wrong argument counts or types for fn, stdlib, effect, and fn-typed-variable calls, wrong-shape callbacks in higher-order slots, interpolation in string patterns, non-scalar interpolation segments, tool `implement` bodies vs the `Result[output, error]` contract, provider bodies vs their declared return, a bare fn name used as a value, and calling a non-function value) | | E0021 | error | Non-exhaustive match | | E0022 | error | Invalid `!` on non-Result | | E0023 | error | Invalid `?` on non-Result, enclosing fn doesn’t return Result, or the propagated error type is incompatible with the enclosing Result’s error type | | E0024 | error | Unknown type name; or non-exhaustive match on an enum/Result/Option missing variant patterns | | E0025 | error | Constraint annotation on wrong type | | E0026 | error | Invalid named argument (unknown/duplicate name, positional after named, callee without named-argument support) | | E0027 | error | Invalid guard expression (guards allow literals, bindings, field access, comparisons, boolean operators, and `+`/`-`/`*` arithmetic) | | E0037 | error | Unverified type at a declared boundary: a value whose type is unknown, or whose branches produced incompatible types, cannot cross a declared fn return | | E0038 | error | Provider contract violation: a scenario `implement` block whose signature does not match its capability’s provider contract, or an `implement` under a capability with no provider contract | ### Agent Errors [Section titled “Agent Errors”](#agent-errors) | Code | Severity | Description | | ----- | -------- | ----------------------------------------------------------------------------- | | E0030 | error | Invalid phase transition | | E0031 | warning | Unreachable phase | | E0032 | error | Phase handler missing | | E0033 | error | `transition()` outside an agent, or in an agent that declares no `Phase` enum | | E0034 | error | `suspend()` outside agent handlers | | E0035 | error | `idempotent()` outside handler bodies | | E0036 | error | `stop()` outside agent handlers | | E0039 | error | `emit` outside agent handlers (module code records events with `event.log`) | ### Supervisor Errors [Section titled “Supervisor Errors”](#supervisor-errors) | Code | Severity | Description | | ----- | -------- | ------------------------------------------------------------------------------------------------------------------------------- | | E0040 | error | Invalid supervisor strategy | | E0041 | error | Invalid `max_restarts` value | | E0042 | warning | Supervisor has no children | | E0043 | error | Invalid `store.table` declaration (typed tables: the capability names a declared record type with exactly one `@primary` field) | ### Warnings [Section titled “Warnings”](#warnings) | Code | Severity | Description | | ----- | -------- | ----------------------------------------------------------------------------------- | | W0001 | warning | Unused binding | | W0002 | warning | Unused capability | | W0003 | warning | Unreachable code after `stop()` | | W0004 | warning | Enum match covers only specific values of a variant (add a binding arm or wildcard) | ### Reserved Codes [Section titled “Reserved Codes”](#reserved-codes) E0013 (capability parameter mismatch) is reserved: the code is allocated and documented, but no compiler path constructs it yet. It keeps its meaning when first emitted — error codes are append-only. # Lexer > How the Skein lexer tokenizes source code. ## Overview [Section titled “Overview”](#overview) The lexer (`Skein.Lexer`) converts UTF-8 source text into a flat list of tokens. It’s implemented as a hand-written tokenizer using binary pattern matching. **Location:** `apps/skein_compiler/lib/skein/lexer.ex` ## Token Format [Section titled “Token Format”](#token-format) Tokens are tuples with positional information: ```elixir # Keywords and punctuation (no value) {:token_type, {line, col}} # Tokens with values {:token_type, {line, col}, value} ``` Line and column numbers are 1-indexed. ### Examples [Section titled “Examples”](#examples) ```elixir Skein.Lexer.tokenize("let x = 42") #=> {:ok, [ # {:let, {1, 1}}, # {:ident, {1, 5}, "x"}, # {:eq, {1, 7}}, # {:int, {1, 9}, 42}, # {:eof, {1, 11}} # ]} ``` ## Token Categories [Section titled “Token Categories”](#token-categories) ### Keywords [Section titled “Keywords”](#keywords) Reserved words that cannot be used as identifiers: ```skein module fn let match type enum handler agent tool capability supervisor test scenario golden on emit transition stop suspend true false implement idempotent ``` > `resume` is not reserved: agents are resumed host-side (`Skein.Runtime.Agent.resume/2`), there is no in-agent `resume` construct, and `resume` is an ordinary identifier in Skein source. Each keyword tokenizes to its corresponding atom: `"module"` becomes `{:module, {line, col}}`. **Contextual keywords** are not reserved. The frozen inventory (spec §2.3, drift-tested against `conformance/freeze/keywords.json`) is `input`, `output`, `errors`, `description`, `state`, `strategy`, `child`, `given`, `expect`, `assert`, `start`, `phase`, `from`, `trace`, `per`, `max_restarts`, and `if` — each has meaning only in its construct and tokenizes as an ordinary `:ident` token (`let input = 1` is valid Skein; `if` introduces match-arm guards). The parser recognizes them positionally. (`replay`, listed in earlier drafts, is not contextual — no construct recognizes it.) ### Identifiers [Section titled “Identifiers”](#identifiers) ```elixir # Lowercase identifiers (variables, function names) {:ident, {1, 1}, "hello"} {:ident, {1, 1}, "my_variable"} # Uppercase identifiers (types, module names) {:upper_ident, {1, 1}, "String"} {:upper_ident, {1, 1}, "UserService"} ``` Lowercase identifiers start with `[a-z_]` and may contain `[a-zA-Z0-9_]` — a leading underscore marks a deliberately-unused binding (spec §2.2, W0001). Uppercase identifiers start with `[A-Z]` and may contain `[a-zA-Z0-9_]`. ### Literals [Section titled “Literals”](#literals) ```elixir # Integers {:int, {1, 1}, 42} # Floats {:float, {1, 1}, 3.14} # Strings (with interpolation segments) {:string, {1, 1}, [{:literal, "Hello, "}, {:interpolation, {:ident, {1, 10}, "name"}}, {:literal, "!"}]} ``` ### Operators and Punctuation [Section titled “Operators and Punctuation”](#operators-and-punctuation) ```elixir # Arithmetic :plus # + :minus # - :star # * :slash # / # Comparison :eq_eq # == :neq # != :lt # < :gt # > :lte # <= :gte # >= # Logical :and_and # && :or_or # || # Assignment and arrows :eq # = :arrow # -> # Punctuation :lbrace # { :rbrace # } :lparen # ( :rparen # ) :lbracket # [ :rbracket # ] :comma # , :dot # . :colon # : :pipe # |> :bang # ! :question # ? :at # @ :ampersand # & # Special :eof # End of input ``` ## String Tokenization [Section titled “String Tokenization”](#string-tokenization) Strings are tokenized with interpolation support. The lexer produces a list of segments: ```elixir # Plain string: "hello" {:string, {1, 1}, [{:literal, "hello"}]} # Interpolated: "Hello, ${name}!" {:string, {1, 1}, [ {:literal, "Hello, "}, {:interpolation, {:ident, {1, 10}, "name"}}, {:literal, "!"} ]} # Empty string: "" {:string, {1, 1}, []} ``` Interpolation uses `${}` syntax. Inside the braces, the lexer produces a nested token for the expression. Identifiers and dotted field access are supported — `${name}` and `${req.params.id}` both work. A dotted expression nests as `{:field_access, ...}` segments: ```elixir # "${req.params.id}" {:interpolation, {:field_access, {:field_access, {:ident, {_line, _col}, "req"}, "params"}, "id"}} ``` ## Comments [Section titled “Comments”](#comments) Single-line comments start with `--` and run to end of line: ```skein -- this is a comment let x = 42 -- inline comment ``` Comments are consumed by the lexer and not included in the token stream. ## Whitespace Handling [Section titled “Whitespace Handling”](#whitespace-handling) Whitespace (spaces, tabs, newlines) is consumed between tokens and used only for position tracking. Newlines increment the line counter and reset the column counter. ## Error Handling [Section titled “Error Handling”](#error-handling) The lexer returns `{:error, [%Skein.Error{}]}` for unrecognized characters: ```elixir Skein.Lexer.tokenize("let x = @#$") #=> {:error, [%Skein.Error{code: "E0001", message: "Unexpected character: #", ...}]} ``` ## Property-Tested Invariants [Section titled “Property-Tested Invariants”](#property-tested-invariants) The lexer has 11 property-based tests verifying: * Any valid lowercase identifier tokenizes to `:ident` * Any valid uppercase identifier tokenizes to `:upper_ident` * Any positive integer tokenizes to `:int` with the correct value * Quoted strings produce `:string` tokens with preserved content * Token lists always end with `:eof` * Token positions are always positive (line >= 1, col >= 1) * All keywords tokenize to their corresponding atoms * Whitespace-separated identifiers produce the correct token count * Newlines correctly increment line numbers * String interpolation preserves identifier names # Parser > How the Skein parser builds ASTs from token streams. ## Overview [Section titled “Overview”](#overview) The parser (`Skein.Parser`) is a hand-written recursive descent parser that converts a token stream into an Abstract Syntax Tree (AST). It uses Pratt-style precedence climbing for expression parsing. **Location:** `apps/skein_compiler/lib/skein/parser.ex` — the largest compiler stage alongside the code generator ## Entry Point [Section titled “Entry Point”](#entry-point) ```elixir {:ok, tokens} = Skein.Lexer.tokenize(source) {:ok, %Skein.AST.Module{}} = Skein.Parser.parse(tokens) # With file name for error reporting {:ok, ast} = Skein.Parser.parse(tokens, "hello.skein") ``` ## Parsing Strategy [Section titled “Parsing Strategy”](#parsing-strategy) ### Recursive Descent [Section titled “Recursive Descent”](#recursive-descent) Each grammar production has a corresponding `parse_*` function: ```plaintext parse_module -> module Name { declarations... } parse_declaration -> fn | type | enum | capability | handler | ... parse_fn -> fn name(params) -> Type { body } parse_block -> { expressions... } parse_expression -> parse_pipe_expr (top of precedence chain) ``` ### Precedence Climbing [Section titled “Precedence Climbing”](#precedence-climbing) Expressions are parsed using a chain of functions, each handling one precedence level. Lower-precedence functions call higher-precedence ones: ```plaintext parse_pipe_expr level 1 (lowest): |> -> parse_or_expr level 2: || -> parse_and_expr level 3: && -> parse_equality level 4: ==, != -> parse_compare level 5: <, >, <=, >= -> parse_add level 6: +, - -> parse_mul level 7: *, / -> parse_unary level 8: !, - (prefix) -> parse_postfix level 9: !, ? (postfix) -> parse_primary level 10: literals, identifiers, calls ``` Each level parses its operator, delegates to the next level for operands, and builds `%AST.BinaryOp{}` or `%AST.UnaryOp{}` nodes. ### Left-Associative Parsing [Section titled “Left-Associative Parsing”](#left-associative-parsing) Binary operators at the same precedence level are left-associative. The parser handles this with a continuation loop: ```plaintext a + b + c -> (a + b) + c BinaryOp(+, BinaryOp(+, a, b), c ) ``` ## What Gets Parsed [Section titled “What Gets Parsed”](#what-gets-parsed) ### Module Declarations [Section titled “Module Declarations”](#module-declarations) ```skein module Hello { ... } ``` Produces `%AST.Module{name: "Hello", declarations: [...], meta: %{...}}`. ### Function Declarations [Section titled “Function Declarations”](#function-declarations) ```skein fn add(a: Int, b: Int) -> Int { a + b } ``` Produces `%AST.Fn{name: "add", params: [...], return_type: %AST.TypeRef{}, body: %AST.Block{}, meta: %{...}}`. Parameters are `%AST.Field{name: "a", type: %AST.TypeRef{name: "Int"}}`. ### Let Bindings [Section titled “Let Bindings”](#let-bindings) ```skein let x = 42 let result = compute(y) ``` Produces `%AST.Let{name: "x", value: %AST.IntLit{value: 42}}`. ### Match Expressions [Section titled “Match Expressions”](#match-expressions) ```skein match expr { true -> "yes" false -> "no" } ``` Produces `%AST.Match{subject: expr, arms: [%AST.MatchArm{pattern: ..., body: ...}, ...]}`. ### Binary Operations [Section titled “Binary Operations”](#binary-operations) ```skein a + b * c ``` Respects precedence: ```elixir %AST.BinaryOp{ op: :+, left: %AST.Identifier{name: "a"}, right: %AST.BinaryOp{ op: :*, left: %AST.Identifier{name: "b"}, right: %AST.Identifier{name: "c"} } } ``` ### Pipe Expressions [Section titled “Pipe Expressions”](#pipe-expressions) ```skein data |> transform() |> validate() ``` Produces a chain of `%AST.Pipe{}` nodes (left-associative). ### Function Calls [Section titled “Function Calls”](#function-calls) ```skein add(3, 4) ``` Produces `%AST.Call{target: %AST.Identifier{name: "add"}, args: [...]}`. ### Field Access [Section titled “Field Access”](#field-access) ```skein user.name user.address.city ``` Produces nested `%AST.FieldAccess{}` nodes. ### String Literals [Section titled “String Literals”](#string-literals) ```skein "Hello, ${name}!" ``` Produces `%AST.StringLit{segments: [{:literal, "Hello, "}, {:interpolation, %AST.Identifier{name: "name"}}, {:literal, "!"}]}`. The parser converts the lexer’s raw interpolation tokens into AST nodes — an `%AST.Identifier{}`, or nested `%AST.FieldAccess{}` for dotted access like `${user.name}`. ### Type and Enum Declarations [Section titled “Type and Enum Declarations”](#type-and-enum-declarations) ```skein type User { name: String age: Int } enum Status { Active Suspended(reason: String) } ``` These parse to `%AST.TypeDecl{}` and `%AST.EnumDecl{}` nodes. ### Capability Declarations [Section titled “Capability Declarations”](#capability-declarations) ```skein capability http.out("api.example.com") ``` Parses to `%AST.Capability{}`. ## Pattern Matching [Section titled “Pattern Matching”](#pattern-matching) Match arm patterns support: | Pattern | Example | AST Node | | -------------------------- | ----------------------------------------- | --------------------------------------------------- | | Boolean literal | `true`, `false` | `%AST.BoolLit{}` | | Integer literal | `0`, `42` | `%AST.IntLit{}` | | String literal | `"active"` | `%AST.StringLit{}` | | Variable binding | `x`, `name` | `%AST.Identifier{}` | | Wildcard | `_` | `%AST.Wildcard{}` | | Enum variant (bare) | `Active`, `Status.Active` | `%AST.Identifier{}` | | Enum variant (with fields) | `Banned(reason)`, `Status.Banned(reason)` | `%AST.Call{target: %AST.Identifier{}, args: [...]}` | Variant field arguments are themselves patterns, so they nest (bindings, literals, wildcards). ### Match Guards [Section titled “Match Guards”](#match-guards) A match arm pattern may carry a guard: `pattern if expr ->`. The contextual keyword `if` between the pattern and the arrow introduces a guard expression, stored on the arm’s `guard` field (`nil` when absent): ```skein match amount { n if n > 100 -> "large" n -> "small" } ``` ## Source Location Tracking [Section titled “Source Location Tracking”](#source-location-tracking) Every AST node carries a `meta` field with source location: ```elixir %{line: 5, col: 3, file: "hello.skein"} ``` This is used for error reporting and debugging. The parser extracts position from each token’s `{line, col}` tuple. ## Error Handling [Section titled “Error Handling”](#error-handling) Parse errors produce structured `%Skein.Error{}` values: ```elixir Skein.Parser.parse([{:ident, {1, 1}, "x"}, {:eof, {1, 2}}]) #=> {:error, [%Skein.Error{message: "Expected 'module', got 'ident'", ...}]} ``` ## Property-Tested Invariants [Section titled “Property-Tested Invariants”](#property-tested-invariants) The parser’s property-based tests verify invariants such as: * Any generated module source lexes and parses successfully * Parsed module name matches the generated name * Number of parsed fn declarations matches the number generated * Every parsed function has a return type * Every parsed function has a block body * Every AST node carries source location metadata * Match expressions on booleans produce exactly 2 arms * Empty modules parse with zero declarations # Compilation Pipeline > How Skein source code becomes running BEAM bytecode. ## Pipeline Overview [Section titled “Pipeline Overview”](#pipeline-overview) ``` flowchart TD A["Source (.skein)"] --> B["Lexer"] B -->|"Token stream"| C["Parser"] C -->|"AST"| D["Analyzer"] D -->|"Annotated AST"| E["CodeGen"] E -->|"Core Erlang AST"| F[":compile.forms"] F -->|"BEAM bytecode"| G[":code.load_binary"] G --> H["Loaded module"] B -.- B1["Source text → Token stream"] C -.- C1["Token stream → AST"] D -.- D1["Type checking, capability checking"] E -.- E1["AST → Core Erlang (via :cerl)"] F -.- F1["Core Erlang → BEAM bytecode"] G -.- G1["BEAM bytecode → Loaded module"] ``` ## Entry Points [Section titled “Entry Points”](#entry-points) The compiler has two entry points in `Skein.Compiler`: ### `compile_string/1` [Section titled “compile\_string/1”](#compile_string1) Compiles a Skein source string to a loaded BEAM module: ```elixir {:module, mod} = Skein.Compiler.compile_string(~S""" module Math { fn add(a: Int, b: Int) -> Int { a + b } } """) mod.add(3, 4) #=> 7 ``` ### `compile_file/1` [Section titled “compile\_file/1”](#compile_file1) Reads a `.skein` file and compiles it: ```elixir {:module, mod} = Skein.Compiler.compile_file("examples/hello.skein") ``` Both return `{:module, module()}` on success or `{:error, [Skein.Error.t()]}` on failure. ## Pipeline Stages [Section titled “Pipeline Stages”](#pipeline-stages) ### Stage 1: Lexer [Section titled “Stage 1: Lexer”](#stage-1-lexer) **Input:** UTF-8 source string **Output:** `{:ok, [token()]}` or `{:error, [Error.t()]}` Converts source text to a flat list of tokens. Each token is a tuple: ```elixir {:keyword, {line, col}} # e.g. {:module, {1, 1}} {:token_type, {line, col}, value} # e.g. {:ident, {1, 8}, "hello"} ``` See [Lexer](/Skein/compiler/lexer/) for details. ### Stage 2: Parser [Section titled “Stage 2: Parser”](#stage-2-parser) **Input:** Token list **Output:** `{:ok, %AST.Module{}}` or `{:error, [Error.t()]}` Builds a structured AST from the token stream using recursive descent with Pratt-style operator precedence. See [Parser](/Skein/compiler/parser/) for details. ### Stage 3: Analyzer [Section titled “Stage 3: Analyzer”](#stage-3-analyzer) **Input:** AST **Output:** `{:ok, annotated_ast}` or `{:error, [Error.t()]}` The analyzer runs a suite of passes over the AST (Passes 0a–10 in the module pass driver, plus a dedicated agent pass suite for agent bodies). Broadly, the passes group into five areas: 1. **Name resolution** — builds a symbol table of all declarations (functions, types, enums), resolves identifier references, and reports unknown identifiers (E0010) and unknown types (E0024) 2. **Type checking** — validates function return types, operator types, match arm consistency, and function call arity, reporting type mismatches (E0020) and non-exhaustive match errors (E0021; E0024 for enums — both are hard errors, not warnings) 3. **Capability checking** — walks function bodies to find effect calls (`http.*`, `store.*`, `memory.*`, `llm.*`, `tool.*`, `queue.*`, `topic.*`, `event.*`, `process.*`, `timer.*`, `uuid.*`, `instant.*`), verifies each has a covering capability declaration, and reports missing capabilities (E0012) with `fix_code` for agent auto-fix 4. **Transition checking** — validates agent `Phase` enum transitions, ensuring `transition(Phase)` calls reference valid target phases reachable from the current phase (E0030) 5. **Lint warnings** — detects unused bindings (W0001), unused capabilities (W0002), and unreachable code after `stop()` (W0003) ### Stage 4: Code Generator [Section titled “Stage 4: Code Generator”](#stage-4-code-generator) **Input:** AST **Output:** `{:ok, [{module_atom, beam_binary}]}` or `{:error, [Error.t()]}` Translates the AST to Core Erlang using the `:cerl` module, then compiles to BEAM bytecode via `:compile.forms/2`. The result is a list of named binaries because one source file can produce several BEAM modules: the primary module comes first, followed by one entry per nested agent. See [Code Generator](/Skein/compiler/codegen/) for details. ### Stage 5: Module Loading [Section titled “Stage 5: Module Loading”](#stage-5-module-loading) The final step uses `:code.load_binary/3` to load each generated binary into the running VM: ```elixir Enum.each(modules, fn {module_name, beam_binary} -> :code.load_binary(module_name, ~c"nofile", beam_binary) end) # The compiler returns {:module, primary_module_atom} ``` Every module (the primary first, then nested agents) is loaded and immediately available for function calls. ## Error Handling [Section titled “Error Handling”](#error-handling) Each stage returns `{:ok, result}` or `{:error, errors}`. The `with` construct chains them: ```elixir def compile_string(source) do with {:ok, tokens} <- Lexer.tokenize(source), {:ok, ast} <- Parser.parse(tokens), {:ok, annotated_ast} <- Analyzer.analyze(ast), {:ok, modules} <- CoreErlang.generate(annotated_ast) do load_modules(modules, ~c"nofile") end end ``` If any stage fails, the error propagates immediately. Errors are `%Skein.Error{}` structs with structured fields for both human and machine consumption. # Development Guide > How to set up, build, test, and contribute to the Skein compiler. ## Environment Setup [Section titled “Environment Setup”](#environment-setup) ### Requirements [Section titled “Requirements”](#requirements) * Elixir 1.19+ on OTP 28+ * Git ### Getting Started [Section titled “Getting Started”](#getting-started) ```bash git clone skein cd skein mix deps.get mix compile ``` ### Dependencies [Section titled “Dependencies”](#dependencies) All dependencies are sourced from [hex.pm](https://hex.pm): ```bash # Fetch dependencies mix deps.get ``` ### Building a Standalone Binary [Section titled “Building a Standalone Binary”](#building-a-standalone-binary) Skein uses [Burrito](https://github.com/burrito-elixir/burrito) to produce self-contained executables that bundle the Erlang runtime: ```bash # Build for the current platform MIX_ENV=prod mix release skein # Build for a specific target BURRITO_TARGET=linux MIX_ENV=prod mix release skein BURRITO_TARGET=macos MIX_ENV=prod mix release skein BURRITO_TARGET=macos_arm MIX_ENV=prod mix release skein ``` Binaries are written to `burrito_out/`. The entry point is `Skein.CLI.Main`, which dispatches subcommands (`compile`, `new`, `build`, `test`, `run`, `agents`, `mcp`, `lsp`, `trace`, `completions`, `version`, `help`). ## Project Layout [Section titled “Project Layout”](#project-layout) Skein is an Elixir umbrella project: ```plaintext skein/ mix.exs # Root umbrella (mix aliases for skein.* commands) apps/ skein_compiler/ # Lexer, parser, analyzer, code generator skein_runtime/ # Agents, HTTP, handlers, store, memory, LLM, tools, trace skein_cli/ # CLI: new, build, test, run, trace skein_lsp/ # Language Server Protocol implementation (GenLSP) editors/ vscode/ # VS Code extension (grammar, snippets, LSP client) docs/ # Specifications and documentation examples/ # Example .skein programs ``` ## Common Tasks [Section titled “Common Tasks”](#common-tasks) ### Running Tests [Section titled “Running Tests”](#running-tests) ```bash # All tests (unit + property; see CI for current counts) mix test # Verbose output mix test --trace # Just compiler tests mix test apps/skein_compiler/test/ # Just runtime tests mix test apps/skein_runtime/test/ # Just CLI tests mix test apps/skein_cli/test/ # Just LSP tests mix test apps/skein_lsp/test/ # Specific test file mix test apps/skein_compiler/test/skein/parser_test.exs # Specific test by line mix test apps/skein_compiler/test/skein/lexer_test.exs:42 ``` ### Formatting Code [Section titled “Formatting Code”](#formatting-code) ```bash mix format ``` Always run `mix format` before committing. The project follows standard Elixir formatting. ### Interactive Development [Section titled “Interactive Development”](#interactive-development) ```bash cd apps/skein_compiler iex -S mix ``` Then in IEx: ```elixir # Compile a string {:module, mod} = Skein.Compiler.compile_string(~S""" module Test { fn hello() -> String { "world" } } """) mod.hello() # Inspect tokens {:ok, tokens} = Skein.Lexer.tokenize(~S|module T { fn f() -> Int { 42 } }|) # Inspect AST (the parser expects a complete top-level module or agent — # parsing a bare expression like `let x = 42` returns {:error, [...]}) {:ok, ast} = Skein.Parser.parse(tokens) ``` ## Coding Conventions [Section titled “Coding Conventions”](#coding-conventions) ### Elixir Style [Section titled “Elixir Style”](#elixir-style) * Follow standard `mix format` * Use `@spec` on all public functions * Use `@moduledoc` and `@doc` on all public modules and functions * Pattern match in function heads over case/cond where possible * Prefer pipe operator for data transformations * No abbreviations except `ctx`, `opts`, `acc` ### Error Handling [Section titled “Error Handling”](#error-handling) * Never raise exceptions for user-facing errors * Use `{:ok, result}` / `{:error, errors}` tuples throughout the pipeline * Every error is a `%Skein.Error{}` struct that can serialize to JSON * Include `fix_hint` and `fix_code` in errors where possible ### Testing [Section titled “Testing”](#testing) * **TDD is mandatory** — write tests before or alongside implementation * Every public function needs happy path and error case tests * Use `~S` sigil for Skein source strings in tests (avoids interpolation conflicts) * CodeGen tests must use `async: false` (they load BEAM modules) * Property tests go alongside unit tests in separate `*_property_test.exs` files ### Git Conventions [Section titled “Git Conventions”](#git-conventions) * Branch naming: `/` (e.g., `compiler/named-args`) * Commit messages: `[component] description` (e.g., `[parser] accept named arguments in calls`) * One roadmap item / issue per PR; reference it with `Closes #NN` ## Architecture Decisions [Section titled “Architecture Decisions”](#architecture-decisions) Key decisions that affect how you work on the compiler: ### Token Format [Section titled “Token Format”](#token-format) Tokens are tuples, not structs: ```elixir {:keyword, {line, col}} # no value {:token_type, {line, col}, value} # with value ``` ### AST Nodes [Section titled “AST Nodes”](#ast-nodes) Every AST node is a struct with a `meta` field: ```elixir %AST.Fn{ name: "add", params: [...], return_type: %AST.TypeRef{}, body: %AST.Block{}, meta: %{line: 1, col: 3, file: "hello.skein"} } ``` ### Core Erlang Generation [Section titled “Core Erlang Generation”](#core-erlang-generation) Use `:cerl` module functions, not text generation: ```elixir # Good: programmatic AST construction :cerl.c_call(:cerl.c_atom(:erlang), :cerl.c_atom(:+), [left, right]) # Bad: string templating "call 'erlang':'+'(#{left}, #{right})" ``` ### Variable Naming [Section titled “Variable Naming”](#variable-naming) Skein `snake_case` becomes Core Erlang `CamelCase`: ```elixir "my_var" -> :MyVar "x" -> :X ``` ### Temporary Variables [Section titled “Temporary Variables”](#temporary-variables) Use `Process.get/put(:skein_var_counter, ...)` for unique variable generation during codegen. ## Working on the Language Server [Section titled “Working on the Language Server”](#working-on-the-language-server) The LSP server lives in `apps/skein_lsp/` and is built with [GenLSP](https://github.com/elixir-tools/gen_lsp). It provides IDE features by calling the Skein compiler API directly. ### Running the LSP [Section titled “Running the LSP”](#running-the-lsp) ```bash # Start the LSP server (stdio transport, used by VS Code) mix skein.lsp ``` ### LSP Modules [Section titled “LSP Modules”](#lsp-modules) | Module | Purpose | | -------------------------- | -------------------------------------------------------------- | | `Skein.Lsp.Server` | Main GenLSP server, protocol request/notification handlers | | `Skein.Lsp.Diagnostics` | Runs lexer→parser→analyzer, converts errors to LSP diagnostics | | `Skein.Lsp.Symbols` | Extracts document symbols from AST | | `Skein.Lsp.HoverProvider` | Hover info and go-to-definition | | `Skein.Lsp.Completions` | Context-aware code completion | | `Skein.Lsp.CodeActions` | Quickfix code actions from each error’s `fix_hint`/`fix_code` | | `Skein.Lsp.SemanticTokens` | Lexer-based semantic token encoding | ### Adding LSP Features [Section titled “Adding LSP Features”](#adding-lsp-features) When adding a new compiler feature, also consider: 1. **Diagnostics** — the LSP automatically picks up new compiler errors 2. **Symbols** — if you add a new top-level construct, add it to `Symbols.document_symbols/1` 3. **Completions** — if you add new keywords or built-in types, add them to `Completions` 4. **Hover** — if you add new built-in types, add descriptions to `HoverProvider` ### VS Code Extension [Section titled “VS Code Extension”](#vs-code-extension) The VS Code extension at `editors/vscode/` has two parts: * **Static**: TextMate grammar (`skein.tmLanguage.json`), snippets (`snippets/skein.json`), language config * **Dynamic**: TypeScript LSP client (`src/extension.ts`) that launches `skein lsp` by default (`mix skein.lsp` inside a compiler checkout) To build the extension: ```bash cd editors/vscode npm install npm run compile ``` ## Adding a New Language Feature [Section titled “Adding a New Language Feature”](#adding-a-new-language-feature) General workflow for extending the compiler: 1. **Write the test first** — what should the feature look like from the user’s perspective? 2. **Extend the lexer** — add any new tokens (if needed) 3. **Extend the parser** — add the grammar production, build AST nodes 4. **Add AST node types** — if new node types are needed 5. **Extend the analyzer** — add type checking / validation 6. **Extend the code generator** — translate the new AST nodes to Core Erlang 7. **Add property tests** — generators that exercise the new feature randomly 8. **Run all tests** — `mix test` must pass with 0 failures ## Useful References [Section titled “Useful References”](#useful-references) * `docs/SKEIN_SPEC.md` — Complete language specification * `docs/ARCHITECTURE.md` — Compiler and runtime architecture * `docs/ROADMAP.md` — Development roadmap * `docs/skein_first_principles.md` — Language design philosophy * Erlang `:cerl` module docs — `https://www.erlang.org/doc/apps/compiler/cerl` * Core Erlang spec — `https://www.it.uu.se/research/group/hipe/cerl/` # Language Server (LSP) > Technical details of the Skein Language Server Protocol implementation. The Skein Language Server is an Elixir application (`skein_lsp`) that implements the [Language Server Protocol](https://microsoft.github.io/language-server-protocol/). It uses the Skein compiler as its analysis engine. ## Capabilities [Section titled “Capabilities”](#capabilities) The server advertises these LSP capabilities: | Capability | Description | | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `textDocument/publishDiagnostics` | Compile errors and warnings | | `textDocument/documentSymbol` | Document outline (modules, functions, handlers) | | `textDocument/hover` | Type information on hover | | `textDocument/definition` | Go-to-definition for functions and types | | `textDocument/completion` | Context-aware code completion | | `textDocument/codeAction` | Quickfixes applied generically from each error’s `span`/`edit_kind`/`fix_code` (with a per-code fallback for span-less diagnostics) | | `textDocument/semanticTokens/full` | Semantic highlighting | | `textDocument/didOpen` | Document tracking | | `textDocument/didChange` | Full document sync | | `textDocument/didSave` | Save-triggered recompilation | | `textDocument/didClose` | Document cleanup | ## How It Works [Section titled “How It Works”](#how-it-works) ### Diagnostics Pipeline [Section titled “Diagnostics Pipeline”](#diagnostics-pipeline) When a document is opened, changed, or saved, the server runs the Skein compiler pipeline: ```plaintext Source text │ ├── Skein.Lexer.tokenize/1 │ └── {:error, errors} → Lexer diagnostics │ ├── Skein.Parser.parse/2 │ └── {:error, errors} → Parser diagnostics │ └── Skein.Analyzer.analyze/1 ├── {:error, errors} → Analyzer diagnostics (AST still retained) └── {:ok, ast} → No diagnostics ``` Each `Skein.Error` is converted to an LSP `Diagnostic` with: * Position mapped from 1-indexed (Skein) to 0-indexed (LSP) * Severity mapped from `:error`/`:warning` to LSP severity levels * Error code and fix hint included in the message ### Symbol Extraction [Section titled “Symbol Extraction”](#symbol-extraction) The server walks the AST to extract symbols for the document outline: | AST Node | Symbol Kind | | ------------------------------ | ----------- | | `Module` | Module | | `Agent` | Class | | `Fn` | Function | | `TypeDecl` | Struct | | `EnumDecl` | Enum | | `Handler` | Event | | `AgentHandler` | Event | | `ToolDecl` | Interface | | `Supervisor` | Namespace | | `Test` / `Scenario` / `Golden` | Function | | `Field` | Field | | `Variant` | EnumMember | | State fields | Property | ### Hover Resolution [Section titled “Hover Resolution”](#hover-resolution) The hover provider uses a name-based lookup strategy: 1. Extract the word at the cursor position 2. Search declarations in the current AST for a matching name 3. If no match, check built-in types (`Int`, `String`, `Option`, etc.) 4. Format the result as a Markdown code block with type signature ### Completion Engine [Section titled “Completion Engine”](#completion-engine) Completions are context-sensitive based on the text before the cursor: | Context | Strategy | | ----------------- | -------------------------------------------------------------- | | After `.` | Look up the namespace and return its methods | | After `:` or `->` | Return type completions | | After `@` | Return annotation names | | General | Keywords + types + user symbols + effect namespaces + snippets | ## Implementation [Section titled “Implementation”](#implementation) The LSP server is built with [GenLSP](https://github.com/elixir-tools/gen_lsp), which provides: * OTP behaviour for LSP process management * Typed structs for all LSP protocol messages * Stdio communication transport * Built-in error handling (exceptions don’t crash the server) ### Dependencies [Section titled “Dependencies”](#dependencies) | Package | Version | Purpose | | ---------------- | ------------ | -------------------------------- | | `gen_lsp` | v0.11.3 | LSP behaviour and protocol types | | `typed_struct` | v0.3.0 | Struct definitions (GenLSP dep) | | `nimble_options` | v1.1.1 | Option validation (GenLSP dep) | | `schematic` | v0.2.1 | Schema validation (GenLSP dep) | | `skein_compiler` | in\_umbrella | Skein lexer, parser, analyzer | ### Running the Server [Section titled “Running the Server”](#running-the-server) ```bash # Via the standalone binary (used by the VS Code extension by default) skein lsp # Via mix task (inside a Skein compiler checkout) mix skein.lsp # Programmatically SkeinLsp.start() ``` The server communicates over stdio using JSON-RPC as specified by the LSP protocol. ## Testing [Section titled “Testing”](#testing) The language server has a test suite covering all features (see CI for current counts): ```bash mix test apps/skein_lsp/test/ ``` Tests exercise each module directly without needing a running LSP process: * **Diagnostics tests** — Valid source produces no diagnostics; invalid source produces correct error codes, locations, and severities * **Symbols tests** — Modules produce correct symbol hierarchies; agents include state fields, phases, and handlers * **Completion tests** — Keywords, types, effect namespaces, and method completions after `.` * **Code action tests** — Generic span/edit\_kind quickfixes (replace, insert, line insert/delete) pinned against real compiler output, plus the per-code fallback for missing tokens (`E0001`), missing capabilities (`E0012`), unused capabilities (`W0002`), and unused bindings (`W0001`) * **Hover tests** — Function signatures, built-in type descriptions * **Semantic token tests** — Valid source encodes to groups-of-5 integers; invalid source returns empty # MCP Server > Register the Skein MCP server with Claude Code, Cursor, and other coding agents for spec lookup, docs search, and structured compile checks. The Skein CLI ships an [MCP](https://modelcontextprotocol.io) (Model Context Protocol) server that gives coding agents on-demand access to the language spec and the compiler’s structured diagnostics — without stuffing the whole spec into the agent’s base prompt. ```bash skein mcp ``` The server speaks JSON-RPC 2.0 over stdio (newline-delimited messages) and needs no network access: the language spec is embedded in the binary at build time. ## Registering [Section titled “Registering”](#registering) ### Claude Code [Section titled “Claude Code”](#claude-code) ```bash claude mcp add skein -- skein mcp ``` Or add it to `.mcp.json` in your project root to share it with your team: ```json { "mcpServers": { "skein": { "command": "skein", "args": ["mcp"] } } } ``` ### Cursor [Section titled “Cursor”](#cursor) Add to `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global): ```json { "mcpServers": { "skein": { "command": "skein", "args": ["mcp"] } } } ``` Other MCP-capable tools (Codex, Windsurf, Zed, …) follow the same pattern: configure a stdio server with command `skein` and arguments `["mcp"]`. ## Tools [Section titled “Tools”](#tools) ### `skein_spec_lookup` [Section titled “skein\_spec\_lookup”](#skein_spec_lookup) Fetches a named section of the language specification. Pass a section number or a title fragment: ```json { "section": "6.4" } { "section": "agents" } ``` If nothing matches, the result lists every available section title, so an agent can self-correct in one round trip. ### `skein_docs_search` [Section titled “skein\_docs\_search”](#skein_docs_search) Case-insensitive search across the spec corpus. Returns each matching section title with the matching lines: ```json { "query": "idempotent" } ``` ### `skein_compile_check` [Section titled “skein\_compile\_check”](#skein_compile_check) Compiles a `.skein` file — or every file under a project’s `src/` when given a directory (checking `src/` and `test/`, matching `skein test`’s discovery) — and returns the compiler’s structured JSON diagnostics directly. Warnings are included; `ok` reflects errors only: ```json { "path": "src/main.skein" } ``` ```json { "ok": false, "files_checked": 1, "errors": [ { "code": "E0012", "severity": "error", "message": "Missing capability declaration for http.out", "location": { "file": "src/main.skein", "line": 3, "col": 13 }, "fix_hint": "Add the capability declaration to the module", "fix_code": "capability http.out(\"example.com\")", "span": { "start": { "line": 2, "col": 3 }, "end": { "line": 2, "col": 3 } }, "edit_kind": "insert_line" } ], "warnings": [ { "code": "W0002", "severity": "warning", "message": "Unused capability 'store.table' — declared but never exercised", "location": { "file": "src/main.skein", "line": 4, "col": 3 }, "fix_hint": "Remove this capability declaration if it is no longer needed", "fix_code": "" } ] } ``` Skein’s compiler errors carry `fix_hint` and `fix_code` precisely so agents can apply fixes mechanically — MCP delivers them without shelling out and parsing CLI output. When the fix is an exact edit, `span` (1-based, end-exclusive) and `edit_kind` say where and how to apply it without any per-error-code logic: `replace` the spanned text with `fix_code`, `insert_before`/`insert_after` it at the span’s edges, `insert_line` it as a new line indented to the span’s start column, or `delete_line` the spanned lines. A `null` `edit_kind` marks `fix_code` as an illustrative template rather than a verbatim edit. ## Related [Section titled “Related”](#related) * `skein new` scaffolds an `AGENTS.md` primer (and a `CLAUDE.md` pointer) into every project; `skein agents` refreshes it after a toolchain upgrade. * Published agent docs: [llms.txt](https://kormie.github.io/Skein/llms.txt), [llms-small.txt](https://kormie.github.io/Skein/llms-small.txt), [llms-full.txt](https://kormie.github.io/Skein/llms-full.txt). # VS Code Extension > Full language support for Skein in Visual Studio Code — syntax highlighting, diagnostics, completions, hover, go-to-definition, and more. The Skein VS Code extension provides a complete editing experience powered by a Language Server Protocol (LSP) implementation built on top of the Skein compiler. ## Installation [Section titled “Installation”](#installation) ### From a GitHub Release (easiest) [Section titled “From a GitHub Release (easiest)”](#from-a-github-release-easiest) Every release ships a prebuilt `skein-vscode.vsix` — download it from the [Releases page](https://github.com/kormie/Skein/releases) and install it with: ```bash code --install-extension skein-vscode.vsix ``` (or in VS Code: **Extensions: Install from VSIX**). ### From Source [Section titled “From Source”](#from-source) ```bash cd editors/vscode npm install npm run package # bundles the extension and builds the .vsix via vsce ``` This writes `skein-lang-.vsix` to `editors/vscode/`. Then in VS Code, use **Extensions: Install from VSIX** or run: ```bash code --install-extension skein-lang-.vsix ``` (A prebuilt `skein-vscode.vsix` is also attached to every [GitHub release](https://github.com/kormie/Skein/releases).) ### Requirements [Section titled “Requirements”](#requirements) * VS Code 1.80+ * For the language server, one of: * the standalone `skein` binary (v0.1.3+) on your `PATH` — no Elixir needed (see the [Quickstart](/Skein/getting-started/quickstart/)), or * an Elixir/OTP checkout of the Skein repo (the extension runs `mix skein.lsp`) Syntax highlighting and snippets work without the language server. ## Features [Section titled “Features”](#features) ### Syntax Highlighting [Section titled “Syntax Highlighting”](#syntax-highlighting) The extension includes a comprehensive TextMate grammar with specialized scoping for all Skein constructs: * **Declarations** — `module`, `agent`, `handler`, `tool`, `supervisor`, `type`, `enum`, `fn` * **Agent constructs** — `on start`, `on phase`, `transition`, `stop`, `emit` * **Handler types** — HTTP (with method + route), queue, schedule, topic * **Effect operations** — `llm.chat`, `memory.put`, `store.get`, `respond.json`, etc. * **String interpolation** — Full recursive highlighting inside `${...}` * **Annotations** — `@description("...")`, `@min(0)`, `@primary` * **Constants** — `true`, `false`, `Ok`, `Err`, `Some`, `None` ### Diagnostics [Section titled “Diagnostics”](#diagnostics) Real-time compiler errors and warnings appear as you type. The language server runs the full Skein compilation pipeline (lexer → parser → analyzer) and reports all errors with: * Error codes (e.g., `E0001`, `E0020`, `E0012`) * Source locations with underline highlights * Fix hints explaining how to resolve the error ```skein module Hello { fn greet() -> String { http.get("https://example.com") -- ^^^^^ [E0012] Missing capability for effect call -- Hint: Add 'capability http.out' to the module } } ``` ### Document Symbols [Section titled “Document Symbols”](#document-symbols) The outline view shows the structure of your `.skein` files: * Modules and agents (top-level) * Functions with parameter signatures * Handlers (HTTP, queue, schedule) * Type and enum declarations with fields/variants * State fields (for agents) * Phase enum with variants * Tests, scenarios, and golden tests Use `Ctrl+Shift+O` (or `Cmd+Shift+O` on macOS) to jump to any symbol. ### Hover Information [Section titled “Hover Information”](#hover-information) Hover over any symbol to see its type information: * **Functions** — Full signature with parameter types and return type * **Types** — Field listing with types * **Enums** — Variant listing * **Phase variants** — Allowed transitions * **Built-in types** — Description (e.g., “64-bit signed integer”) * **State fields** — Type annotation ### Go-to-Definition [Section titled “Go-to-Definition”](#go-to-definition) `Ctrl+Click` (or `F12`) on a function or type name to jump to its definition within the file. ### Code Completion [Section titled “Code Completion”](#code-completion) Context-aware completions triggered automatically or with `Ctrl+Space`: | Context | Completions | | ------------- | ------------------------------------------------------------- | | General | Keywords, types, user-defined symbols, effect namespaces | | After `.` | Methods for the namespace (`llm.` → `chat`, `json`, `stream`) | | After `@` | Annotation names (`@description`, `@min`, `@max`, etc.) | | Type position | Built-in and user-defined types | Supported effect namespaces: `llm`, `memory`, `store`, `http`, `topic`, `queue`, `trace`, `event`, `process`, `timer`, `respond`. ### Quickfix Code Actions [Section titled “Quickfix Code Actions”](#quickfix-code-actions) Diagnostics with a mechanical fix offer a one-click quickfix (the lightbulb, or `Ctrl+.`), built from the `fix_hint`/`fix_code` that every Skein error carries: | Code | Diagnostic | Quickfix | | ------- | ------------------ | ---------------------------------------------------------------------- | | `E0001` | Missing token | Inserts the missing token after the keyword named in the message | | `E0012` | Missing capability | Inserts the `capability ...` declaration line into the module or agent | | `W0002` | Unused capability | Deletes the unused declaration line | | `W0001` | Unused binding | Renames the binding to its underscore-prefixed form | Diagnostics without a mapped fix simply offer no action. ### Semantic Tokens [Section titled “Semantic Tokens”](#semantic-tokens) Enhanced highlighting beyond TextMate grammar, powered by the Skein lexer. Provides accurate token classification for keywords, types, variables, operators, and more. ### Snippets [Section titled “Snippets”](#snippets) 30+ snippets for common patterns. Type the prefix and press `Tab`: | Prefix | Expands to | | ------------------- | ------------------------------------------- | | `module` | Module scaffold | | `agent` | Agent with state, phases, and start handler | | `fn` | Function definition | | `let` | Let binding | | `match` | Match expression with arms | | `handler http get` | HTTP GET handler | | `handler http post` | HTTP POST handler | | `handler queue` | Queue handler | | `handler schedule` | Schedule handler | | `tool` | Full tool declaration | | `supervisor` | Supervisor declaration | | `test` | Test with assert | | `scenario` | Scenario with given/expect | | `golden` | Golden trace test | | `emit` | Emit event | | `transition` | Phase transition | | `respond` | JSON response | | `llm.chat` | LLM chat call | | `llm.json` | Structured LLM call | | `memory.put` | Store to memory | | `memory.get` | Read from memory | | `capability` | Capability declaration (with choices) | | `httpmodule` | Full HTTP module with health check | ## Configuration [Section titled “Configuration”](#configuration) | Setting | Default | Description | | ------------------------- | --------- | -------------------------------------------------------------------------------------------------- | | `skein.lsp.enabled` | `true` | Enable/disable the language server | | `skein.lsp.serverCommand` | `"auto"` | `skein` (standalone binary), `mix` (compiler checkout), or `auto` (mix only inside the Skein repo) | | `skein.lsp.skeinPath` | `"skein"` | Path to the `skein` binary | | `skein.lsp.path` | `""` | Working directory for the server (defaults to workspace root) | | `skein.lsp.mixCommand` | `"mix"` | Path to the `mix` executable (mix mode only) | | `skein.trace.server` | `"off"` | LSP trace level: `off`, `messages`, `verbose` | ## Architecture [Section titled “Architecture”](#architecture) The extension has two components: 1. **VS Code Client** (TypeScript, `editors/vscode/src/extension.ts`) — Manages the editor integration and communicates with the language server over stdio 2. **Skein Language Server** (Elixir, `apps/skein_lsp/`) — An OTP application built with [GenLSP](https://github.com/elixir-tools/gen_lsp) that uses the Skein compiler for analysis ### Language Server Modules [Section titled “Language Server Modules”](#language-server-modules) | Module | Purpose | | -------------------------- | ------------------------------------------------------------- | | `Skein.Lsp.Server` | Main LSP server — request/notification handlers | | `Skein.Lsp.Diagnostics` | Compiler error → LSP diagnostic conversion | | `Skein.Lsp.Symbols` | AST → document symbol extraction | | `Skein.Lsp.HoverProvider` | Symbol resolution, hover info, go-to-definition | | `Skein.Lsp.Completions` | Context-aware completion engine | | `Skein.Lsp.CodeActions` | Quickfix code actions from each error’s `fix_hint`/`fix_code` | | `Skein.Lsp.SemanticTokens` | Lexer-based semantic token encoding | ### Starting the Language Server [Section titled “Starting the Language Server”](#starting-the-language-server) By default the extension starts the language server via the standalone `skein` binary (`skein lsp`); `mix skein.lsp` is only used inside a checkout of the Skein compiler repo (see the `skein.lsp.serverCommand` setting). Either way it communicates over stdin/stdout: ```bash # Start manually (for debugging) skein lsp # Inside a Skein compiler checkout mix skein.lsp ``` The VS Code extension starts this automatically when a `.skein` file is opened. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) ### Language server not starting [Section titled “Language server not starting”](#language-server-not-starting) 1. Ensure the `skein` binary is on your `PATH` (`skein version` should print v0.1.3+), or — when using mix mode — that Elixir is installed and `mix` is on your `PATH` 2. Check the Output panel (**View > Output**, select “Skein Language Server”), or run **Skein: Show Language Server Output** from the command palette 3. Set `skein.trace.server` to `"verbose"` for detailed protocol logs 4. If VS Code was launched from the dock rather than a terminal, it may not see your shell’s `PATH` — set `skein.lsp.skeinPath` to the binary’s absolute path ### Diagnostics not appearing [Section titled “Diagnostics not appearing”](#diagnostics-not-appearing) 1. Check that `skein.lsp.enabled` is `true` 2. Verify the file has a `.skein` extension 3. In mix mode, check `skein.lsp.path` points to a Skein compiler checkout # Overview > What Skein is, what it can do today, and where it's headed. ## What is Skein? [Section titled “What is Skein?”](#what-is-skein) Skein is a new programming language that compiles to BEAM bytecode and runs on the Erlang VM. It ships as a standalone binary for Linux and macOS — no Erlang or Elixir install required. Install it with one command — `curl -fsSL https://kormie.github.io/Skein/install.sh | sh` — or grab a binary from the [Releases page](https://github.com/kormie/Skein/releases), and you’re ready to go. See the [Quickstart](/Skein/getting-started/quickstart/) to get running in under 5 minutes. Skein is designed around six ranked principles: | Priority | Principle | Summary | | -------- | ------------------------- | ---------------------------------------------------------------------- | | P1 | One Obvious Way | One idiomatic way to express each task — no synonyms, no sugar | | P2 | Spec Fits in Context | The complete language spec fits in 128K tokens (an LLM context window) | | P3 | Types Are Contracts | Types serve the compiler, generate schemas, and constrain agent code | | P4 | Effects Are Visible | Every side effect is declared, traced, and replayable | | P5 | Crash Gracefully | OTP’s “let it crash” philosophy is the default for agent workloads | | P6 | Humans Read, Agents Write | Syntax favors regularity and unambiguous parsing over cleverness | ## What Works Today [Section titled “What Works Today”](#what-works-today) The compilation pipeline is fully operational. You can write `.skein` files with modules, functions, types, tools, supervisors, HTTP/queue/schedule handlers, store operations, and agents — compile them to BEAM bytecode, and run them on a Bandit + Plug HTTP server. Store tables are typed — `capability store.table("users", User)` names the record type, operations are type-checked at compile time, and writes are schema-checked at runtime — backed by the in-memory ETS runtime store. The CLI tooling provides project scaffolding, building, testing, running, and trace inspection. Test constructs include `test`, `scenario` (with `given`/`expect`), and `golden` trace tests with a deterministic replay engine. **Language constructs:** * `module Name { ... }` — module declarations * `fn name(arg: Type) -> ReturnType { ... }` — function declarations * `let x = expr` — immutable bindings * `match expr { pattern -> expr }` — pattern matching (the only conditional) * `expr |> fn(args)` — pipe operator * `"string ${interpolation}"` — string interpolation * `type Name { fields }` — record type declarations * `enum Name { variants }` — enum type declarations with transitions * `capability namespace.kind(params)` / `capability model(provider, model)` — capability declarations * `handler http METHOD "/path" (req) -> { ... }` — HTTP handler declarations * `handler queue "queue-name" (msg) -> { ... }` — queue handler declarations * `handler schedule "cron-expr" () -> { ... }` — schedule handler declarations * `test "description" { ... }` — inline test declarations * `scenario "description" { given { ... } expect { ... } }` — scenario tests * `golden "description" from trace "file" { ... }` — golden trace tests * `agent Name { ... }` — agent state machines with phases and handlers * `tool Name.Action { ... }` — typed tool declarations with `input`/`output` schemas, the cross-module call seam * `supervisor Name { ... }` — supervision trees with `child` entries, restart policies, strategy, and `max_restarts` * All arithmetic operators: `+`, `-`, `*`, `/` * All comparison operators: `==`, `!=`, `<`, `>`, `<=`, `>=` * Logical operators: `&&`, `||` * Unary operators: `!` (prefix not), `!` and `?` (postfix) * Function calls: `f(x, y)` * Field access: `x.field` * Function references: `&function_name` **Type system:** * Built-in types: `String`, `Int`, `Float`, `Bool`, `Uuid`, `Instant`, `Duration`, `Email`, `Url` * Parameterized types: `Option[T]`, `Result[T, E]`, `List[T]`, `Map[K, V]`, `Set[T]` * Type checking at function boundaries and operator validation * Type inference — field access resolves types through record definitions * JSON schema derivation from type declarations * Constraint annotations: `@min`, `@max`, `@one_of`, `@default`, `@primary`, `@unique`, `@description` **Capabilities and effects:** * Compile-time capability checking (missing capability = structured error with `fix_code`) * HTTP effect calls: `http.get`, `http.post`, `http.put`, `http.patch`, `http.delete` * Store effect calls: `store.
.get`, `store.
.put`, `store.
.delete`, `store.
.query` * Memory effect calls: `memory.put`, `memory.get`, `memory.delete`, `memory.list` * LLM effect calls: `llm.chat`, `llm.json`, `llm.stream`, `llm.embed` * Runtime capability enforcement across all subsystems (HTTP, store, memory, LLM) * Automatic trace recording for every effect call with timing, outcome, and token usage **Agents:** * `agent` declarations with `Phase` enums and `->` transitions * `on start(params)` and `on phase(Phase)` handlers * Compile-time phase transition validation * `transition(Phase)`, `stop()`, `suspend(reason)`, `emit(event)`, `state.field` access * `suspend(reason)` for human-in-the-loop workflows — the host resumes a suspended agent from outside via `Skein.Runtime.Agent.resume(pid, phase)` * `gen_statem`-based runtime with automatic phase handler dispatch **Runtime:** * HTTP client with capability enforcement and tracing * Handler dispatch with route matching and path parameters * Queue dispatch with subscribe/publish for event-driven handlers * Schedule dispatch with cron expression parsing for time-triggered handlers * ETS-backed store with capability-gated CRUD operations and schema-checked typed-table writes * Scoped KV memory with namespace isolation and capability enforcement * LLM client with pluggable backends — Anthropic (production default), OpenAI-compatible (local model servers + embeddings), and AWS Bedrock — and schema-validated JSON responses * Agent runtime wrapping `:gen_statem` for phase-based state machines * Real OTP supervision from `supervisor` declarations — `skein run` boots them via `Skein.Runtime.SupervisorHost` with restart policies and `max_restarts` intensity * Opt-in SQLite persistence for the event log — `skein run` writes events to `.skein/events.db` by default (`--no-persist` opts out) and reloads history on restart * Bandit + Plug HTTP server with `req.json[T]` body validation * Trace recording with timing, outcome, and token usage metadata * Trace enrichment with model-specific usage data (input/output tokens, cost) * Replay engine for deterministic trace playback in tests **CLI tooling:** * `skein new ` — scaffold a new project with `skein.toml`, `src/`, `test/`, `AGENTS.md`, `CLAUDE.md`, `README.md`, and `.gitignore` (git-inits by default) * `skein compile ` — compile a single `.skein` file * `skein build [dir]` — compile all `.skein` files in a project’s `src/` tree * `skein test [dir]` — discover and run all tests across `src/` and `test/` directories * `skein run [dir]` — compile and start an HTTP server for handler modules * `skein trace` — view recent trace spans with `--last` and `--kind` filters * `skein agents [dir]` — create or refresh `AGENTS.md` in an existing project * `skein mcp` — MCP server over stdio, so coding agents can compile, test, and trace Skein projects * `skein lsp` — language server over stdio, for editor integrations * `skein completions zsh` — print the zsh shell completion script * `skein version` / `skein help` — version and usage info **Editor tooling:** * VS Code extension with syntax highlighting, 30+ snippets, and full LSP integration * Language Server Protocol (LSP) providing diagnostics, document symbols, hover, go-to-definition, code completion, and semantic tokens * LSP server built with [GenLSP](https://github.com/elixir-tools/gen_lsp), using the Skein compiler for analysis See the [Editor Support](/Skein/editor/vscode/) docs for setup instructions. ## What’s Not Built Yet [Section titled “What’s Not Built Yet”](#whats-not-built-yet) * Agent pool supervision (`AgentPool` with max concurrency) Tool `policy` blocks (rate limits, approval workflows) were **cut from the language** (#319) — runtime policy enforcement is out of the language surface, not forthcoming. See the [Roadmap](/Skein/roadmap/overview/) for the full plan. ## Technology Stack [Section titled “Technology Stack”](#technology-stack) | Component | Choice | Rationale | | ----------------- | -------------------------------------- | ------------------------------------------------------------- | | Compiler language | Elixir | Team familiarity, excellent BEAM tooling | | Lexer | Hand-written (binary pattern matching) | Fast, dependency-free tokenizing with precise error positions | | Parser | Hand-written recursive descent | Better error messages than parser generators | | IR target | Core Erlang | Standard BEAM compilation target (used by Elixir, Gleam, LFE) | | Language server | GenLSP | OTP behaviour for LSP implementations | | Testing | ExUnit + StreamData + PropCheck | Unit + property-based + stateful testing | # Project Structure > How the Skein codebase is organized. ## Umbrella Layout [Section titled “Umbrella Layout”](#umbrella-layout) Skein is an Elixir umbrella project with four apps: ```plaintext skein/ ├── mix.exs # Root umbrella config ├── CLAUDE.md # Project instructions and conventions ├── docs/ │ ├── SKEIN_SPEC.md # Complete language specification │ ├── ARCHITECTURE.md # Compiler and runtime architecture │ ├── ROADMAP.md # Canonical prioritized work list │ ├── STABILITY.md # Versioning and stability policy │ ├── skein_first_principles.md # Language design philosophy │ └── site/ # This documentation site (Astro + Starlight) ├── examples/ # Canonical .skein programs (all compile-tested) │ ├── hello.skein │ ├── hello_http.skein │ ├── hello_llm.skein │ ├── refund_agent.skein │ ├── incident_triage.skein │ ├── queue_worker.skein │ ├── supervisor_pool.skein │ ├── pubsub_notifications.skein │ ├── semantic_search.skein │ ├── ... │ └── market_research/ # Multi-file example (agent + service) ├── editors/ │ └── vscode/ # VS Code extension │ ├── package.json # Extension manifest │ ├── skein.tmLanguage.json # TextMate grammar │ ├── snippets/skein.json # 30+ snippets │ └── src/extension.ts # LSP client (TypeScript) └── apps/ ├── skein_compiler/ # Lexer, parser, analyzer, code generator ├── skein_runtime/ # OTP behaviours and runtime support ├── skein_cli/ # CLI tooling (skein new, build, test, run, ...) └── skein_lsp/ # Language Server Protocol implementation ``` ## Compiler App (`apps/skein_compiler/`) [Section titled “Compiler App (apps/skein\_compiler/)”](#compiler-app-appsskein_compiler) The full pipeline: source text → tokens → AST → annotated AST → Core Erlang → BEAM bytecode. ```plaintext skein_compiler/ ├── mix.exs ├── lib/ │ ├── skein_compiler.ex # Entry point: compile_string/1, compile_file/1 │ └── skein/ │ ├── lexer.ex # Tokenizer (hand-written binary matching) │ ├── parser.ex # Hand-written recursive descent parser │ ├── ast.ex # AST node struct definitions │ ├── analyzer.ex # Type, capability, transition, and guard checking │ ├── error.ex # Structured, JSON-serializable error type │ └── codegen/ │ ├── core_erlang.ex # AST -> Core Erlang -> BEAM bytecode │ └── schema_gen.ex # Type declarations -> JSON Schema └── test/ # Unit, integration, and property-based tests ``` The analyzer is the largest stage of the pipeline and one of Skein’s headline features: it performs type checking and inference, capability checking (every effect call must be covered by a declared capability), agent phase transition validation, and match/guard analysis — all before any code is generated. ## Runtime App (`apps/skein_runtime/`) [Section titled “Runtime App (apps/skein\_runtime/)”](#runtime-app-appsskein_runtime) OTP behaviours and libraries that compiled Skein programs link against: the `gen_statem`-based agent runtime, handler dispatch (HTTP/queue/schedule), the tool registry, runtime capability enforcement, scoped KV memory, the schema-checked ETS-backed store, the LLM client with pluggable backends, the trace/event store with replay, and the Bandit + Plug HTTP server. ## CLI App (`apps/skein_cli/`) [Section titled “CLI App (apps/skein\_cli/)”](#cli-app-appsskein_cli) The `skein` command-line tool: project scaffolding (`new`, `agents`), compiling (`compile`, `build`), testing (`test`), running (`run`), trace inspection (`trace`), shell `completions`, and stdio servers for editors and coding agents (`lsp`, `mcp`). Packaged as a standalone binary with Burrito. ## LSP App (`apps/skein_lsp/`) [Section titled “LSP App (apps/skein\_lsp/)”](#lsp-app-appsskein_lsp) The language server behind the VS Code extension — diagnostics, hover, go-to-definition, completions, document symbols, and semantic tokens, built on GenLSP and reusing the compiler for analysis. ## Dependencies [Section titled “Dependencies”](#dependencies) All dependencies are sourced from [hex.pm](https://hex.pm): ### Compiler (`skein_compiler`) [Section titled “Compiler (skein\_compiler)”](#compiler-skein_compiler) | Dependency | Version | Purpose | | ------------- | ------- | ---------------------------------------------------- | | `jason` | \~> 1.4 | JSON encoding for structured errors and schema gen | | `stream_data` | \~> 1.1 | Property-based testing generators (test/dev only) | | `propcheck` | \~> 1.4 | Stateful property testing via PropEr (test/dev only) | ### Runtime (`skein_runtime`) [Section titled “Runtime (skein\_runtime)”](#runtime-skein_runtime) | Dependency | Version | Purpose | | ------------------- | -------- | ---------------------------- | | `bandit` | \~> 1.6 | HTTP server | | `plug` | \~> 1.16 | HTTP middleware | | `ecto` / `ecto_sql` | \~> 3.12 | Data layer | | `ecto_sqlite3` | \~> 0.17 | SQLite adapter for local dev | | `telemetry` | \~> 1.3 | Instrumentation | ### CLI (`skein_cli`) [Section titled “CLI (skein\_cli)”](#cli-skein_cli) | Dependency | Version | Purpose | | ---------- | ------- | ------------------------------------------------------ | | `burrito` | \~> 1.5 | Standalone binary packaging (wraps OTP release + ERTS) | ## Key Files [Section titled “Key Files”](#key-files) ### `lib/skein_compiler.ex` [Section titled “lib/skein\_compiler.ex”](#libskein_compilerex) The main entry point orchestrating the pipeline: ```elixir def compile_string(source) do with {:ok, tokens} <- Lexer.tokenize(source), {:ok, ast} <- Parser.parse(tokens), {:ok, annotated_ast} <- Analyzer.analyze(ast, source_text: source), {:ok, modules} <- CoreErlang.generate(annotated_ast) do load_modules(modules, ~c"nofile") end end ``` Returns `{:module, module()}` on success or `{:error, [Skein.Error.t()]}` on failure. Code generation can produce multiple BEAM modules from one source file (agents nested in a module compile to their own modules). ### `lib/skein/ast.ex` [Section titled “lib/skein/ast.ex”](#libskeinastex) All AST node types as Elixir structs. Every node has a `meta` field carrying `%{line: int, col: int, file: string}` for source location tracking. Key nodes: * **Top-level:** `Module`, `Fn`, `TypeDecl`, `EnumDecl`, `Capability`, `Agent`, `ToolDecl`, `Supervisor`, `Handler`, `Test`, `Scenario`, `Golden` * **Expressions:** `BinaryOp`, `UnaryOp`, `Call`, `Pipe`, `FieldAccess`, `Let`, `Match`, `Block` * **Literals:** `IntLit`, `FloatLit`, `BoolLit`, `StringLit`, `ListLit`, `MapLit`, `Identifier` * **Types:** `TypeRef`, `Field`, `Variant`, `Annotation` ### `lib/skein/error.ex` [Section titled “lib/skein/error.ex”](#libskeinerrorex) Structured errors designed for both human and LLM consumption: ```elixir %Skein.Error{ code: "E0001", severity: :error, message: "Unexpected character: ;", location: %{file: "hello.skein", line: 5, col: 12}, fix_hint: "Skein does not use semicolons; a statement ends at the end of the line", fix_code: "" } ``` # Quickstart > Build and run your first Skein program in under 5 minutes. ## Install [Section titled “Install”](#install) One command — detects your platform, verifies the SHA-256 against the release checksums, and installs to `~/.local/bin` (no root): ```bash curl -fsSL https://kormie.github.io/Skein/install.sh | sh skein version # → skein 1.0.0-rc.5 ``` Pin a version with `SKEIN_VERSION=0.4.0`, or change the destination with `SKEIN_BIN_DIR=/usr/local/bin`. The script is [`install.sh`](https://github.com/kormie/Skein/blob/main/install.sh) if you want to read it first. Prefer manual installation? Grab the binary for your platform from the [Releases page](https://github.com/kormie/Skein/releases) (`skein-{linux,macos}-{x86_64,aarch64}`), `chmod +x` it, and put it on your PATH. No Erlang, Elixir, or other dependencies required — it’s a self-contained binary. ## Create a Project [Section titled “Create a Project”](#create-a-project) ```bash skein new hello_world cd hello_world ``` This gives you: ```plaintext hello_world/ skein.toml # project config (incl. LLM backend) src/main.skein # module + tool + co-located test test/main_test.skein # integration test through the module's tool AGENTS.md # Skein primer for coding agents CLAUDE.md # one-line pointer to AGENTS.md README.md .gitignore ``` The scaffold already runs: `src/main.skein` declares a function, a `test` block next to it, and a tool — the one cross-module seam in Skein — and `test/main_test.skein` exercises that tool the way another service or agent would. The project is also `git init`-ed for you (skip with `--no-git`), and `AGENTS.md` / `CLAUDE.md` can be skipped with `--no-agents` or regenerated later in any project with `skein agents`. ## Write Your First Program [Section titled “Write Your First Program”](#write-your-first-program) Add functions to the `HelloWorld` module in `src/main.skein` (keep the scaffolded tool and test — `test/main_test.skein` calls the tool): ```skein module HelloWorld { fn hello(name: String) -> String { "Hello, ${name}!" } fn add(a: Int, b: Int) -> Int { a + b } fn classify(n: Int) -> String { match n > 0 { true -> "positive" false -> "non-positive" } } -- Tools are how other modules and agents call into this one. tool HelloWorld.Greet { description: "Greet a person by name" input { name: String } output { greeting: String } implement { Ok({ greeting: hello(name) }) } } -- Tests live with the code they exercise. test "hello returns greeting" { assert hello("World") == "Hello, World!" } } ``` ## Compile [Section titled “Compile”](#compile) ```bash skein compile src/main.skein # → Compiled: Skein.User.HelloWorld ``` ## Build the Whole Project [Section titled “Build the Whole Project”](#build-the-whole-project) ```bash skein build ``` This compiles every `.skein` file in `src/`. ## Run Tests [Section titled “Run Tests”](#run-tests) ```bash skein test ``` Compiles and loads everything in `src/` and `test/` first, then runs every `test` block found — co-located tests in `src/` and integration tests in `test/` alike. On a fresh scaffold that’s two tests: the module’s own `test` block and the cross-module `tool.call` test. ## Start a Server [Section titled “Start a Server”](#start-a-server) If your project has HTTP, queue, or schedule handlers: ```bash skein run ``` The command compiles the project, starts the service on port 4000 by default (change with `--port`), and stays in the foreground while it runs. ## Inspect Traces [Section titled “Inspect Traces”](#inspect-traces) Every effect (LLM calls, HTTP requests, store operations) is automatically traced: ```bash skein trace --last 5 skein trace --kind llm ``` ## All Commands [Section titled “All Commands”](#all-commands) ```bash skein help ``` ```plaintext Skein 1.0.0-rc.5 — AI-native language for the BEAM Usage: skein [options] Commands: compile Compile a single .skein file new Scaffold a new Skein project build [project-dir] Compile all .skein files in a project (default: .) test [project-dir] Run all tests in a project (default: .) run [project-dir] Start the Skein service (default: .) agents [project-dir] Create or refresh AGENTS.md (default: .) mcp Start the MCP server (stdio, for coding agents) lsp Start the language server (stdio, for editors) trace [options] View recent trace spans completions zsh Print the zsh completion script version Print version help Show this help Options: new --backend LLM backend in skein.toml: anthropic (default), bedrock, openai_compatible, test new --no-agents Skip generating AGENTS.md / CLAUDE.md new --no-git Skip git init (a .gitignore is always written) build --output Write .beam files to directory run --port Server port (default: 4000) run --no-persist Skip SQLite event persistence (default: events persist to .skein/events.db) trace --last Number of traces (default: 10) trace --kind Filter by span kind --json Machine-readable JSON output (compile, build, test, trace) — a versioned, documented envelope ``` ## Building from Source [Section titled “Building from Source”](#building-from-source) If you want to hack on the compiler itself: ```bash git clone https://github.com/kormie/Skein.git cd Skein # mise reads .mise.toml for the right Erlang/Elixir versions mise install mise exec -- mix deps.get mise exec -- mix compile mise exec -- mix test ``` The project is an Elixir umbrella with four apps: ```plaintext apps/ skein_compiler/ # Lexer, parser, analyzer, code generator skein_runtime/ # Agents, HTTP, store, memory, LLM, tracing skein_cli/ # CLI tooling (new, build, test, run, trace, agents, mcp, ...) skein_lsp/ # Language Server Protocol implementation ``` ## Next Steps [Section titled “Next Steps”](#next-steps) * [Language Guide](/Skein/language/syntax/) — every construct explained * [Overview](/Skein/getting-started/overview/) — full feature inventory * [Editor Support](/Skein/editor/vscode/) — VS Code extension with LSP # Agents > Declaring and building stateful AI agents in Skein with phases, transitions, LLM integration, memory, tools, and events. ## Overview [Section titled “Overview”](#overview) Agents are Skein’s core construct for building **stateful, phase-driven AI services**. An agent is a state machine where each phase can call LLMs, invoke tools, read/write memory, emit events, and transition to the next phase. At runtime, agents compile to OTP `gen_statem` processes — battle-tested Erlang state machines with built-in supervision and fault tolerance. Agents are designed for workflows like: * Evaluating a refund request by analyzing a ticket, deciding eligibility, and issuing the refund * Triaging an incident by classifying severity, routing to the right team, and escalating if needed * Processing an order through validation, payment, fulfillment, and notification stages ## Agent Syntax [Section titled “Agent Syntax”](#agent-syntax) ```skein agent { capability state { : } enum Phase { -> [, ...] ... } on start() -> { -- initialization logic transition(Phase.) } on phase(Phase.) -> { -- phase logic transition(Phase.) } fn () -> { -- helper functions } } ``` ### Required Elements [Section titled “Required Elements”](#required-elements) Every agent must declare: | Element | Purpose | | -------------------- | ------------------------------------------------------- | | `enum Phase { ... }` | Defines valid states and transitions between them | | `on start(...)` | Initialization handler — runs when the agent is created | | `on phase(Phase.X)` | Phase handler — runs when the agent enters phase X | ### Optional Elements [Section titled “Optional Elements”](#optional-elements) | Element | Purpose | | --------------- | ------------------------------------------------------------- | | `capability` | Declares effects the agent may use (LLM, memory, HTTP, etc.) | | `state { ... }` | Declares the agent’s state field schema (see [State](#state)) | | `fn` | Helper functions callable from within or outside the agent | ## Phase Enum and Transitions [Section titled “Phase Enum and Transitions”](#phase-enum-and-transitions) The `Phase` enum declares which states the agent can be in and which transitions are valid. The `->` syntax specifies allowed target phases. ```skein enum Phase { Analyze -> [Refund, Done] -- Analyze can go to Refund or Done Refund -> [Done, Failed] -- Refund can go to Done or Failed Failed -> [Analyze] -- Failed can retry by going back to Analyze Done -> [] -- Done is terminal (no transitions out) } ``` ### Compile-Time Validation [Section titled “Compile-Time Validation”](#compile-time-validation) The Skein analyzer validates transitions at compile time: * **Invalid transition (E0030):** Transitioning to a phase that is not in the current phase’s declared target list is a compile error. With the enum above, `transition(Phase.Failed)` inside `on phase(Phase.Analyze)` is an E0030 error because `Failed` is not in `Analyze`’s target list (`Analyze -> [Refund, Done]`). * **Unreachable phase (E0031):** A phase that no other phase can transition to (and isn’t the start target) generates a warning. * **Missing phase handler (E0032):** Every phase variant must have a corresponding `on phase(Phase.X)` handler. ## Lifecycle [Section titled “Lifecycle”](#lifecycle) ### Starting an Agent [Section titled “Starting an Agent”](#starting-an-agent) The `on start` handler runs once when the agent is created. It receives typed parameters and must transition to an initial phase (or stop). Start parameters are bindings local to the `on start` body — write anything later phases need to `memory.kv`: ```skein agent OrderProcessor { capability memory.kv("orders") enum Phase { Validate -> [Process, Reject] Process -> [Done] Reject -> [Done] Done -> [] } on start(order_id: String, amount: Int) -> { memory.put("order_id", order_id) memory.put("amount", amount) transition(Phase.Validate) } on phase(Phase.Validate) -> { let amount = memory.get("amount")! match amount > 0 { true -> transition(Phase.Process) false -> transition(Phase.Reject) } } on phase(Phase.Process) -> { transition(Phase.Done) } on phase(Phase.Reject) -> { transition(Phase.Done) } on phase(Phase.Done) -> { stop() } } ``` ### Phase Execution [Section titled “Phase Execution”](#phase-execution) When a phase handler runs via `transition(Phase.X)`, the gen\_statem moves to state `:x` and queues an internal `:execute_phase` event. The handler for that phase then executes. This means phase handlers run **sequentially** — one phase completes before the next begins. ### Stopping [Section titled “Stopping”](#stopping) Call `stop()` to terminate the agent normally. This is typically done in terminal phases: ```skein on phase(Phase.Done) -> { stop() } ``` ### Suspending and Resuming [Section titled “Suspending and Resuming”](#suspending-and-resuming) Call `suspend(reason)` to pause an agent for human-in-the-loop review or external input. The agent enters a `:suspended` state and waits until resumed externally: ```skein on phase(Phase.Failed) -> { suspend("Requires human review") } ``` A suspended agent stays alive but does not execute any phase handlers until resumed from outside. This is the primary mechanism for pausing agent workflows that need human intervention. Resume from Elixir: ```elixir Skein.Runtime.Agent.resume(pid, :analyze) ``` See [Runtime > Agents: Suspending and Resuming](/Skein/runtime/agents/#suspending-and-resuming) for the full runtime API (`is_suspended?/1`, `get_suspend_reason/1`, `resume/2`). ### Control Flow Summary [Section titled “Control Flow Summary”](#control-flow-summary) | Construct | Meaning | | --------------------------------- | --------------------------------------- | | `transition(Phase.X)` | Move to phase X and execute its handler | | `stop()` | Terminate the agent process normally | | `suspend(reason)` | Pause the agent for external input | | `emit EventName { field: value }` | Record a domain event | ## State [Section titled “State”](#state) The `state` block declares the agent’s typed state field schema. In 1.0 it is a **declaration only**: start parameters are not merged into the runtime state map (`get_state/1` returns `%{}`), and `transition(Phase.X)` takes exactly one argument — the target phase — so transitions cannot carry state updates either. The supported way to carry data across phases is `memory.kv`, the same pattern the canonical `examples/refund_agent.skein` uses — write in `on start`, read in phase handlers: ```skein agent RefundBot { capability memory.kv("refunds") enum Phase { Analyze -> [Done] Done -> [] } on start(ticket_id: Uuid, customer_id: String) -> { memory.put("ticket_id", ticket_id) memory.put("customer_id", customer_id) transition(Phase.Analyze) } on phase(Phase.Analyze) -> { let ticket_id = memory.get("ticket_id")! let customer_id = memory.get("customer_id")! transition(Phase.Done) } on phase(Phase.Done) -> { stop() } } ``` ## Capabilities [Section titled “Capabilities”](#capabilities) Agents declare capabilities just like modules. Common agent capabilities: | Capability | Purpose | | ------------------------------------------- | ---------------------------------------------- | | `capability model("claude-opus-4-8")` | Use an LLM model | | `capability memory.kv("sessions")` | Scoped key-value storage | | `capability http.out("api.example.com")` | Make outbound HTTP calls | | `capability store.table("tickets", Ticket)` | Database storage, typed by the `Ticket` record | | `capability tool.use(Stripe.CreateRefund)` | Call a declared tool | ```skein agent Classifier { capability model("anthropic", "claude-opus-4-8") capability memory.kv("classifications") enum Phase { Classify -> [] } on start(text: String) -> { memory.put("text", text) transition(Phase.Classify) } on phase(Phase.Classify) -> { let text = memory.get("text")! let label = llm.chat("claude-opus-4-8", "Classify this text.", text)! memory.put("latest_label", label) stop() } } ``` ## LLM Integration [Section titled “LLM Integration”](#llm-integration) Agents can call LLM models for decision-making via the `llm.*` effects: ### `llm.chat` — Unstructured Text [Section titled “llm.chat — Unstructured Text”](#llmchat--unstructured-text) ```skein on phase(Phase.Analyze) -> { let text = memory.get("text")! let analysis = llm.chat("claude-opus-4-8", "Analyze this input", text)! -- analysis is a String } ``` ### `llm.json` — Schema-Constrained JSON [Section titled “llm.json — Schema-Constrained JSON”](#llmjson--schema-constrained-json) ```skein type Decision { action: String @one_of(["approve", "deny"]) amount: Int @min(0) reason: String } on phase(Phase.Decide) -> { let description = memory.get("ticket_description")! let decision = llm.json[Decision]( "claude-opus-4-8", "Decide if this warrants a refund", description )! -- decision is validated against the Decision type's JSON Schema } ``` ### `llm.stream` — Streaming Responses [Section titled “llm.stream — Streaming Responses”](#llmstream--streaming-responses) ```skein on phase(Phase.Generate) -> { let data = memory.get("data")! let result = llm.stream("claude-opus-4-8", "Generate a report", data) -- chunks are delivered to the runtime callback; result is the assembled text } ``` All LLM calls require a `capability model("model-name")` declaration and are automatically traced. ## Memory [Section titled “Memory”](#memory) Agents use `memory.kv` for scoped key-value storage that persists across phase transitions: ```skein agent SessionTracker { capability memory.kv("sessions") enum Phase { Active -> [] } on start(user_id: String) -> { memory.put("current_user", user_id) transition(Phase.Active) } on phase(Phase.Active) -> { let user = memory.get("current_user")! let keys = memory.list("user:") memory.delete("old_key") stop() } } ``` Each namespace requires a `memory.kv` capability declaration. Memory is backed by a single ETS table (`:skein_memory`) keyed by `{namespace, key}`. ## Events [Section titled “Events”](#events) Use `emit` to record domain events. Events are append-only and queryable via `get_events/1` on the agent process: ```skein on phase(Phase.Approved) -> { let ticket_id = memory.get("ticket_id")! let amount = memory.get("amount")! emit RefundIssued { ticket_id: ticket_id, amount: amount, refund_id: "ref_123" } transition(Phase.Done) } ``` Events accumulate in the agent’s event log across all phase transitions. ## Tool Calling [Section titled “Tool Calling”](#tool-calling) Agents can invoke declared tools for external integrations: ```skein agent PaymentAgent { capability tool.use(Stripe.CreateRefund) capability memory.kv("payments") enum Phase { Refund -> [Done, Failed] Done -> [] Failed -> [] } on start(customer_id: String, amount: Int) -> { memory.put("customer_id", customer_id) memory.put("amount", amount) transition(Phase.Refund) } on phase(Phase.Refund) -> { let customer_id = memory.get("customer_id")! let amount = memory.get("amount")! let result = tool.call(Stripe.CreateRefund, { customer_id: customer_id, amount: amount }) match result { Ok(refund) -> transition(Phase.Done) Err(e) -> transition(Phase.Failed) } } on phase(Phase.Done) -> { stop() } on phase(Phase.Failed) -> { stop() } } ``` Tool calls require `capability tool.use(ToolName)` and are traced with timing and outcome. ## Helper Functions [Section titled “Helper Functions”](#helper-functions) Agents can declare `fn` functions, callable from the agent’s own handlers and other helper functions (and, for a nested agent, the enclosing module’s `fn`s are callable too): ```skein agent UtilAgent { enum Phase { Init -> [] } fn add(a: Int, b: Int) -> Int { a + b } fn greet(name: String) -> String { "Hello, ${name}!" } on start() -> { transition(Phase.Init) } on phase(Phase.Init) -> { stop() } } ``` ```elixir # Called from Elixir Skein.Agent.UtilAgent.add(3, 4) #=> 7 ``` ## Complete Example: Refund Agent [Section titled “Complete Example: Refund Agent”](#complete-example-refund-agent) ```skein module Refunds { type RefundDecision { action: String @one_of(["approve", "deny"]) amount: Int @min(0) } agent RefundAgent { capability model("anthropic", "claude-opus-4-8") capability memory.kv("refund_sessions") capability tool.use(Stripe.CreateRefund) enum Phase { Analyze -> [Refund, Done] Refund -> [Done, Failed] Failed -> [Analyze] Done -> [] } on start(ticket_id: Uuid, customer_id: String) -> { memory.put("ticket_id", ticket_id) memory.put("customer_id", customer_id) transition(Phase.Analyze) } on phase(Phase.Analyze) -> { let ticket_id = memory.get("ticket_id")! let decision = llm.json[RefundDecision]( "claude-opus-4-8", "Decide if this ticket warrants a refund.", ticket_id )! memory.put("decision_action", decision.action) memory.put("decision_amount", decision.amount) match decision.action { "approve" -> transition(Phase.Refund) "deny" -> transition(Phase.Done) } } on phase(Phase.Refund) -> { let ticket_id = memory.get("ticket_id")! let customer_id = memory.get("customer_id")! let amount = memory.get("decision_amount")! let result = tool.call(Stripe.CreateRefund, { customer_id: customer_id, amount: amount }) match result { Ok(refund) -> { emit RefundIssued { ticket_id: ticket_id, refund_id: refund.id } transition(Phase.Done) } Err(e) -> { emit RefundFailed { ticket_id: ticket_id, error: e } transition(Phase.Failed) } } } on phase(Phase.Failed) -> { transition(Phase.Analyze) } on phase(Phase.Done) -> { stop() } } } ``` ## Compilation [Section titled “Compilation”](#compilation) A top-level agent named `RefundAgent` compiles to `Elixir.Skein.Agent.RefundAgent`; nested inside `module Refunds` (as above) it compiles to `Elixir.Skein.Agent.Refunds.RefundAgent`. Either way the module carries these generated functions: | Function | Purpose | | --------------------- | --------------------------------------------------------- | | `start_link/1` | Start the agent with initial params | | `__phases__/0` | Return phase metadata (variants and valid transitions) | | `__capabilities__/0` | Return declared capabilities | | `__start_handler__/2` | Execute the `on start(...)` handler | | `__phase_handler__/3` | Execute phase-specific handlers, dispatched by phase atom | ### Using from Elixir [Section titled “Using from Elixir”](#using-from-elixir) ```elixir # Start the agent {:ok, pid} = Skein.Agent.Refunds.RefundAgent.start_link(%{ ticket_id: "ticket-123", customer_id: "cust-456" }) # Query state (while agent is alive) Skein.Runtime.Agent.get_phase(pid) #=> :analyze # The runtime state map stays empty in 1.0 — agent data lives in memory.kv Skein.Runtime.Agent.get_state(pid) #=> %{} # Emitted events are keyed by :event Skein.Runtime.Agent.get_events(pid) #=> [%{event: "RefundIssued", ticket_id: "ticket-123", refund_id: "ref_789"}] ``` ## Supervision [Section titled “Supervision”](#supervision) Supervisor declarations can name agents as children. The declaration is validated at compile time and emitted as `__supervisors__/0` metadata, and under `skein run` the runtime boots a real OTP supervisor from it ([#325](https://github.com/kormie/Skein/issues/325)) — `Skein.Runtime.SupervisorHost` starts each child agent with its declared restart policy (`permanent`, `transient`, `temporary`), applies the `strategy:` and `max_restarts:` intensity, and appends a `:child_started` event to the EventStore on every start and restart: ```skein supervisor RefundPool { child RefundAgent { restart: permanent } strategy: one_for_one max_restarts: 5 per 60s } ``` See [Supervisors](/Skein/language/supervisors/) for details. ## Testing Agents [Section titled “Testing Agents”](#testing-agents) Agents are tested using Skein’s built-in test constructs. Test blocks are module-level declarations, so they live inside a module: ```skein module RefundAgentTests { test "refund agent approves valid refund" { -- Unit tests can compile and start agents assert true == true } scenario "refund flow" { given { ticket_id: "t-123" customer_id: "c-456" } expect { assert ticket_id == "t-123" } } } ``` For deterministic testing, the LLM client uses a pluggable backend system. In tests, `Skein.Runtime.Llm.TestBackend` returns canned responses, ensuring agent phase transitions are deterministic and reproducible. See [Testing](/Skein/language/testing/) for the full testing guide. # Capabilities and Effects > How Skein enforces capability-based security for side effects like HTTP calls and store operations. ## Overview [Section titled “Overview”](#overview) Skein uses a **capability system** to control side effects. Any function that performs I/O — HTTP requests, database access, file operations — must be authorized by a capability declaration at the module level. This is checked at both compile time and runtime. This design ensures: * **Visibility:** You can see exactly what a module can do by reading its capability declarations * **Security:** Modules cannot perform undeclared effects, even if code is injected * **Auditability:** Every effect call is traced with timing and outcome ## Capability Declarations [Section titled “Capability Declarations”](#capability-declarations) Capabilities are declared at the top of a module with the `capability` keyword: ```skein module UserService { capability http.out("api.example.com") capability http.out("auth.example.com") fn fetch_user(id: String) -> Result[String, HttpError] { http.get("https://api.example.com/users/${id}") } } ``` ### Syntax [Section titled “Syntax”](#syntax) ```skein capability . capability .() ``` ### Available Capabilities [Section titled “Available Capabilities”](#available-capabilities) | Capability | Purpose | Parameters | | ------------------ | ---------------------------------------------- | --------------------------------------------------------------------- | | `http.out` | Outbound HTTP requests | Host allowlist (optional) | | `http.in` | Inbound HTTP handlers | Route prefix (optional) | | `store.table` | Database table access | Table name, record type (a declared `type` with one `@primary` field) | | `memory.kv` | Scoped KV memory | Namespace name | | `model` | LLM model access | Provider, model identifier | | `tool.use` | Tool execution | Tool name | | `queue.publish` | Publish to a named queue | Queue name | | `queue.consume` | Inbound queue handlers | Queue name (optional) | | `topic.publish` | Publish to a named topic | Topic name | | `topic.consume` | Subscribe to a named topic | Topic name | | `schedule.trigger` | Cron-based schedule handlers | Cron expression (optional) | | `process.spawn` | Spawn supervised processes | Pool name (optional) | | `timer` | One-shot and recurring timers | Timer namespace (optional) | | `event.log` | Structured event logging | Log namespace (optional) | | `uuid` | `uuid.new()` — capability-gated randomness | None | | `instant` | `instant.now()` — capability-gated clock reads | None | ### Wildcard Capabilities [Section titled “Wildcard Capabilities”](#wildcard-capabilities) A capability without parameters acts as a wildcard: ```skein module OpenClient { capability http.out -- allows HTTP to any host fn fetch(url: String) -> Result[String, HttpError] { http.get(url) } } ``` Use wildcard capabilities sparingly — explicit host lists are preferred for security. ## Effect Calls [Section titled “Effect Calls”](#effect-calls) Effect calls look like regular function calls with a namespace prefix. ### HTTP Effects [Section titled “HTTP Effects”](#http-effects) ```skein http.get(url) http.post(url, body) http.put(url, body) http.patch(url, body) http.delete(url) ``` ### Store Effects [Section titled “Store Effects”](#store-effects) ```skein store.users.get(id) store.users.put(record) store.users.delete(id) store.users.query(filter) ``` ### Memory Effects [Section titled “Memory Effects”](#memory-effects) ```skein memory.put(key, value) memory.get(key) memory.delete(key) memory.list(prefix) ``` Memory is scoped by namespace, but the namespace never appears at the call site: the `capability memory.kv("namespace")` declaration names it, and the compiler threads it into every memory call. ### LLM Effects [Section titled “LLM Effects”](#llm-effects) ```skein llm.chat("claude-opus-4-8", "system prompt", input) llm.json[RefundDecision]("claude-opus-4-8", "system prompt", input) llm.stream("claude-opus-4-8", "system prompt", input) llm.embed("voyage-3-large", input) ``` `llm.chat` returns unstructured text. `llm.json[T]` returns a parsed map constrained by a JSON schema derived from type `T` at compile time. `llm.stream` returns the assembled response text after streaming all chunks. `llm.embed` returns a vector (list of floats) for use in semantic search and RAG patterns. All four require a `capability model(...)` declaration. #### Type-Parameterized JSON (`llm.json[T]`) [Section titled “Type-Parameterized JSON (llm.json\[T\])”](#type-parameterized-json-llmjsont) The `llm.json[T]` syntax takes a type parameter in square brackets. The compiler looks up the type declaration and generates the corresponding JSON Schema, which is passed to the LLM runtime for schema-constrained decoding: ```skein module RefundService { type RefundDecision { action: String @one_of(["approve", "deny", "escalate"]) amount: Int @min(0) reason: String } capability model("anthropic", "claude-opus-4-8") fn decide(ticket: String) -> RefundDecision { llm.json[RefundDecision]("claude-opus-4-8", "Decide if this warrants a refund.", ticket)! } } ``` The generated schema for `RefundDecision` includes all type information and constraint annotations: ```json { "type": "object", "properties": { "action": { "type": "string", "enum": ["approve", "deny", "escalate"] }, "amount": { "type": "integer", "minimum": 0 }, "reason": { "type": "string" } }, "required": ["action", "amount", "reason"] } ``` You can also call `llm.json` without a type parameter for untyped JSON responses: ```skein llm.json("claude-opus-4-8", "Return JSON.", input) ``` This uses an empty schema (`{}`) which accepts any valid JSON. ### Request Body Validation (`req.json[T]`) [Section titled “Request Body Validation (req.json\[T\])”](#request-body-validation-reqjsont) Inside HTTP handlers, `req.json[T]` parses the request body as JSON and validates it against the schema derived from type `T`: ```skein module UserService { capability http.in type CreateUser { email: String name: String } handler http POST "/users" (req) -> { let user = req.json[CreateUser]! respond.json(201, user) } } ``` The compiler generates a JSON Schema from `CreateUser` at compile time. At runtime, `req.json[T]` parses the request body and validates the required fields and types. It returns `{:ok, parsed}` or `{:error, reason}`, making it compatible with the `!` (crash) and `?` (propagate) unwrap operators. ### Tool Effects [Section titled “Tool Effects”](#tool-effects) ```skein tool.call(CreateRefund, args) tool.list() tool.schema(CreateRefund) ``` Tool effects require a `capability tool.use(ToolName)` declaration. See the [Tools](/Skein/language/tools/) page for full documentation on tool declarations and calling. ### Topics [Section titled “Topics”](#topics) Topics provide pub/sub messaging with fan-out delivery. Every subscriber to a topic receives every published message. ```skein -- Publishing requires topic.publish capability capability topic.publish("order.events") topic.publish("order.events", data) ``` The `topic.publish(name, data)` effect publishes `data` to all handlers subscribed to the named topic. Unlike queues (which deliver to a single consumer), topics broadcast to every subscriber. To consume from a topic, declare a `topic.consume` capability and a `handler topic` block: ```skein capability topic.consume("order.events") handler topic "order.events" (msg) -> { -- Every subscriber receives every message respond.json(200, "processed") } ``` See the [Handlers](/Skein/language/handlers/) page for full topic handler documentation. ### Trace [Section titled “Trace”](#trace) `trace.annotate(key, value)` adds key-value metadata to the trace log. Unlike other effects, trace annotations do **not** require a capability declaration — they are always available in any module, agent, or handler. ```skein module UserService { capability http.in handler http GET "/greet/:name" (req) -> { let name = req.params.name trace.annotate("user", name) respond.json(200, "hello") } } ``` Annotations are recorded as trace entries with `kind: :annotation` alongside regular effect spans. They appear in `Skein.Runtime.Trace.recent_spans/1` output interleaved with HTTP, memory, and LLM spans. In agents, use `trace.annotate` to enrich trace context with business-relevant data: ```skein agent RefundAgent { capability model("claude-opus-4-8") capability memory.kv enum Phase { Review -> [Approved] Approved -> [] } on start(order_id: String) -> { memory.put("order_id", order_id) transition(Phase.Review) } on phase(Phase.Review) -> { let order = memory.get("order_id")! trace.annotate("ticket_id", order) let decision = llm.chat("claude-opus-4-8", "Evaluate refund", order)! trace.annotate("decision", decision) memory.put("decision", decision) transition(Phase.Approved) } on phase(Phase.Approved) -> { stop() } } ``` ### Process Spawning [Section titled “Process Spawning”](#process-spawning) `process.spawn` spawns a supervised background task. Spawned processes are automatically supervised and traced, providing crash isolation. ```skein module BackgroundWorker { capability process.spawn("workers") fn start_task(data: String) -> Result[String, String] { process.spawn(data) } } ``` Requires a `capability process.spawn(...)` declaration. Spawned processes run under a DynamicSupervisor with `:temporary` restart strategy — they are not restarted on crash. ### Timer Effects [Section titled “Timer Effects”](#timer-effects) Timer effects schedule one-shot or recurring callbacks: ```skein module TimerService { capability timer("maintenance") fn schedule_task() -> Result[String, String] { timer.after(5000, "callback") -- fires once after 5 seconds timer.interval(30000, "callback") -- fires every 30 seconds timer.cancel("timer-ref") -- cancels a timer by ref } } ``` * `timer.after(delay_ms, task)` returns `Result[String, String]` — `Ok` carries the timer ref * `timer.interval(interval_ms, task)` returns `Result[String, String]` — `Ok` carries the timer ref * `timer.cancel(timer_ref)` returns `Result[String, String]` — idempotent; `Ok` carries the ref back, `Err` is a scope-label denial All three require a `capability timer(...)` declaration. ### Event Logging [Section titled “Event Logging”](#event-logging) `event.log(name, data)` records a structured event to the event log and returns `Result[String, String]` (`Ok` carries the event name; a scope-label denial is an `Err`). Events have automatic timestamps and unique IDs. ```skein module AuditService { capability http.in capability event.log("audit") handler http POST "/login" (req) -> { event.log("user.login", req) respond.json(200, "ok") } handler http POST "/action" (req) -> { event.log("user.action", req) respond.json(200, "recorded") } } ``` Requires a `capability event.log(...)` declaration. Events are stored in the unified event store (`Skein.Runtime.EventStore`) as `kind: :user_event` entries, queryable alongside trace spans, annotations, and memory state changes via `EventStore.query(kind: :user_event)`. ### How They Parse [Section titled “How They Parse”](#how-they-parse) Effect calls use existing Skein syntax — no special grammar is needed. `http.get(url)` parses as: ```plaintext Call { target: FieldAccess { subject: Identifier { name: "http" }, field: "get" }, args: [Identifier { name: "url" }] } ``` The analyzer recognizes this pattern and checks it against declared capabilities. ### Return Values [Section titled “Return Values”](#return-values) **HTTP** effect calls return `Result[HttpResponse, HttpError]`. Since C2 (#297) the error side is the frozen structured-error ABI, not a bare string: * `{:ok, response}` on success — an `HttpResponse` record with `status`, `body`, and `headers` * `{:error, }` on failure — `Timeout`, `ConnectionFailed`, `Status(code, body)` for non-2xx responses, `InvalidRequest(reason)`, or `Denied(reason)` for capability violations; each lowers to its snake\_case tuple (e.g. `{:status, 404, body}`, `{:denied, reason}`) so Skein `Err(HttpError.Status(code, body))` arms really match **Memory** effect calls return `Result` tuples typed `Result[T, MemoryError]`: * `memory.get` returns `{:ok, value}` or `{:error, :not_found}` — matched as `Err(MemoryError.NotFound)` (or the bare `Err(NotFound)`) in Skein * `memory.put` returns `{:ok, value}` * `memory.delete` returns `{:ok, key}` * `memory.list` returns a list of matching keys * a namespace denial is `{:error, {:denied, reason}}` — `Err(MemoryError.Denied(reason))` **LLM** effect calls return `Result` tuples typed `Result[_, LlmError]`. Since C2 (#297) the error side is the **frozen structured-error ABI**: nominal enum variants that lower to snake\_case tuples, so a Skein `Err(LlmError.ProviderError(code, message))` arm really matches. * `llm.chat` returns `{:ok, response_text}` or `{:error, }` — e.g. `{:rate_limit, retry_after_ms}`, `{:provider_error, code, message}`, `{:denied, reason}` * `llm.json` returns `{:ok, parsed_map}` or `{:error, }` * `llm.stream` returns `{:ok, assembled_text}` or `{:error, }` (chunks delivered via callback at runtime) * `llm.embed` returns `{:ok, [float()]}` or `{:error, }` (vector dimensionality depends on model) The full variant list for every effect error enum (`HttpError`, `LlmError`, `ToolError`, `StoreError`, `MemoryError`, `PublishError`) is in spec §6 and pinned by `Skein.EffectABI.error_enums/0`. ## Compile-Time Checking [Section titled “Compile-Time Checking”](#compile-time-checking) The analyzer’s capability checking pass (Pass 3) walks every function body looking for effect calls. When it finds one, it checks that the module declares the required capability. ### Error: Missing Capability (E0012) [Section titled “Error: Missing Capability (E0012)”](#error-missing-capability-e0012) ```skein module BadService { fn fetch(url: String) -> Result[String, HttpError] { http.get(url) -- ERROR: E0012 } } ``` Produces: ```json { "code": "E0012", "severity": "error", "message": "Capability 'http.out' required but not declared. Effect calls to 'http' require this capability.", "fix_hint": "Add a capability declaration to the module: capability http.out", "fix_code": "capability http.out" } ``` The `fix_code` field gives agents the exact text to add to fix the error. ### What Gets Checked [Section titled “What Gets Checked”](#what-gets-checked) The analyzer detects effect calls in all positions: * Top-level expressions in function bodies * Inside `let` bindings: `let result = http.get(url)` * Inside `match` arms: `true -> http.get(url)` * Inside pipe chains: `url |> http.get()` (the piped value becomes the call’s first argument) * Nested within other expressions ## Runtime Enforcement [Section titled “Runtime Enforcement”](#runtime-enforcement) Even if the compiler is bypassed, the runtime enforces capabilities as a second layer of defense. When compiled code calls `http.get(url)`, the code generator emits a call to `Skein.Runtime.Http.get(url, capabilities)` where `capabilities` is the module’s declared capability list. The runtime checks the URL’s host against this list before making the request. ```skein -- This compiles, but the runtime blocks it: -- The URL host "api.blocked.com" doesn't match "api.allowed.com" module Service { capability http.out("api.allowed.com") fn fetch(url: String) -> Result[String, HttpError] { http.get("https://api.blocked.com/data") } } ``` ### How It Works [Section titled “How It Works”](#how-it-works) 1. The code generator embeds capabilities in each module via `__capabilities__/0` 2. Effect calls compile to runtime module calls with capabilities as a parameter 3. The runtime validates the URL host against declared hosts 4. Blocked requests return `{:error, {:denied, "Host 'x' not declared in http.out capabilities"}}` — `Err(HttpError.Denied(reason))` in Skein ## Effect Tracing [Section titled “Effect Tracing”](#effect-tracing) Every effect call is automatically traced. The runtime records: | Field | Description | | ------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `kind` | Event type (`:http`, `:memory`, `:llm`, `:store`, `:tool`, `:annotation`, `:user_event`, `:state_change`, `:process`, `:timer`) | | `method` | Operation (`:get`, `:post`, `:put`, `:chat`, `:json`, `:embed`, `:call`, etc.) | | `url` | Target URL (HTTP) | | `namespace` | Memory namespace (Memory) | | `model` | LLM model name (LLM) | | `name` | Tool name (Tool) | | `key` | Annotation key (Trace annotations) | | `value` | Annotation value (Trace annotations) | | `duration_us` | Wall-clock duration in microseconds | | `outcome` | `:ok` or `:error` | | `timestamp` | Monotonic timestamp | All events are stored in the unified event store (`Skein.Runtime.EventStore`) and can be queried via `EventStore.query/1`, `EventStore.recent/1`, or `Trace.recent_spans/1`. Memory state can be reconstructed from `:state_change` events via `Memory.rebuild_from_events/1`. ## How Modules Compile (with Capabilities) [Section titled “How Modules Compile (with Capabilities)”](#how-modules-compile-with-capabilities) A module with capabilities compiles to a BEAM module that includes: 1. All user-defined functions (as before) 2. `__info__/1` for Elixir compatibility (as before) 3. **`__capabilities__/0`** — returns the declared capabilities as a list of maps ```elixir # Calling __capabilities__/0 on a compiled module: Skein.User.MyService.__capabilities__() #=> [%{kind: "http.out", params: ["api.example.com"]}] ``` Effect calls in function bodies compile to remote calls: ```skein -- Skein source http.get(url) -- Compiles to (conceptually): Skein.Runtime.Http.get(url, [%{kind: "http.out", params: ["api.example.com"]}]) ``` ## Idempotency [Section titled “Idempotency”](#idempotency) The `idempotent(key)` expression is a built-in guard for handler bodies that ensures exactly-once processing. It does not require a capability declaration — it is available in any handler. ```skein handler queue "events" (msg) -> { idempotent(msg.id) -- rest of handler only runs once per unique msg.id } ``` The idempotency guard is backed by `Skein.Runtime.Idempotent`, which tracks processed keys in an ETS table with a configurable TTL (default: 1 hour). See the [Handlers](/Skein/language/handlers/#idempotent-handlers) page for full details. # Expressions > All expression types in Skein and how they compile to BEAM code. ## Expression-Oriented Language [Section titled “Expression-Oriented Language”](#expression-oriented-language) Skein is expression-oriented — everything produces a value. The last expression in a block is its return value. There is no `return` keyword. ```skein fn abs_value(n: Int) -> Int { match n > 0 { true -> n false -> 0 - n } } ``` ## Literals [Section titled “Literals”](#literals) ### Integers [Section titled “Integers”](#integers) ```skein 42 0 -7 1000 ``` Compile to Erlang integers via `:cerl.abstract/1`. ### Booleans [Section titled “Booleans”](#booleans) ```skein true false ``` Compile to Erlang atoms `true` and `false`. ### Strings [Section titled “Strings”](#strings) Plain strings: ```skein "hello world" ``` Strings with interpolation: ```skein "Hello, ${name}!" "id: ${user.id}" ``` Interpolation accepts an identifier with optional dot access (`${name}`, `${user.id}`) — not arbitrary expressions. To interpolate a computed value, bind it first: ```skein let result = a + b "Result: ${result}" ``` String interpolation compiles to `erlang:iolist_to_binary/1` over an iolist. For example, `"Hello, ${name}!"` compiles to: ```erlang call 'erlang':'iolist_to_binary'([<<"Hello, ">>, Name, <<"!">>]) ``` This approach handles mixed literal and dynamic segments efficiently. ## Binary Operations [Section titled “Binary Operations”](#binary-operations) All binary operations compile to Core Erlang calls to the corresponding Erlang BIF: | Skein | Core Erlang | | ---------- | ------------------------------------------------------------------------------------ | | `a + b` | `call 'erlang':'+'(A, B)` | | `a - b` | `call 'erlang':'-'(A, B)` | | `a * b` | `call 'erlang':'*'(A, B)` | | `a / b` | runtime dispatch: `'erlang':'/'` if either operand is a float, else `'erlang':'div'` | | `a == b` | `call 'erlang':'=='(A, B)` | | `a != b` | `call 'erlang':'/='(A, B)` | | `a < b` | `call 'erlang':'<'(A, B)` | | `a > b` | `call 'erlang':'>'(A, B)` | | `a <= b` | `call 'erlang':'=<'(A, B)` | | `a >= b` | `call 'erlang':'>='(A, B)` | | `a && b` | `call 'erlang':'and'(A, B)` | | `a \|\| b` | `call 'erlang':'or'(A, B)` | Note the Erlang-specific mappings: `!=` becomes `'/='` and `<=` becomes `'=<'`. Division compiles to a runtime dispatch — float operands use Erlang `'/'` (float division), integer operands use `'div'` (integer division). ## Let Bindings [Section titled “Let Bindings”](#let-bindings) Let bindings compile to Core Erlang `let` expressions: ```skein let doubled = x + x doubled * 3 ``` Becomes: ```erlang let = call 'erlang':'+'(X, X) in call 'erlang':'*'(Doubled, 3) ``` ## Match Expressions [Section titled “Match Expressions”](#match-expressions) Match compiles to Core Erlang `case` expressions: ```skein match n > 0 { true -> "positive" false -> "non-positive" } ``` Becomes: ```erlang case call 'erlang':'>'(N, 0) of <'true'> when 'true' -> <<"positive">> <'false'> when 'true' -> <<"non-positive">> end ``` Match arms can have: * **Boolean patterns:** `true`, `false` * **Integer patterns:** `0`, `42` * **String patterns:** `"active"`, `"paused"` * **Variable patterns:** `x` (binds the matched value) * **Wildcard pattern:** `_` (matches anything, discards value) * **Enum variant patterns:** `Active`, `Status.Active` (matches atom variants) * **Enum variant with fields:** `Event.Charge(amt)` (destructures into bound variables) ### Enum Variant Patterns [Section titled “Enum Variant Patterns”](#enum-variant-patterns) Enum variants can be pattern matched with optional field destructuring: ```skein enum Result { Ok(value: Int) Err(message: String) } fn unwrap_or(r: Result, default: Int) -> Int { match r { Result.Ok(v) -> v Result.Err(msg) -> default } } ``` Simple variants without fields match as atoms: ```skein enum Status { Active Inactive } fn is_active(s: Status) -> Bool { match s { Active -> true _ -> false } } ``` Variant patterns compile to Core Erlang tuple patterns. For example, `Result.Ok(v)` becomes a pattern matching the tuple `{:ok, V}`. ### Guards [Section titled “Guards”](#guards) A match arm may carry a guard: the arm is selected only when the pattern matches *and* the guard evaluates to `true`. A failing guard falls through to the later arms. ```skein match order.total { t if t > 1000 -> "review" t if t > 0 -> "approve" _ -> "reject" } ``` Guards see the pattern’s bindings and must be `Bool`. They are restricted to a guard-safe subset — literals, bindings, field access, comparisons, boolean operators, and `+`/`-`/`*` arithmetic. Calls, effects, division, and string interpolation in a guard are compile errors (`E0027`); compute those values in a `let` before the match. A guarded arm does not count toward exhaustiveness, and a `match` where every arm’s guard fails raises `case_clause` at runtime. See [Types > Enum Variant Matching](/Skein/language/types/#enum-variant-matching) for details on exhaustiveness checking. ## Function Calls [Section titled “Function Calls”](#function-calls) Local function calls compile to Core Erlang `apply`: ```skein add(3, 4) ``` Becomes: ```erlang apply 'add'/2(3, 4) ``` ### Named Arguments [Section titled “Named Arguments”](#named-arguments) Arguments can be passed by name, in any order, after any positional arguments: ```skein fn describe(name: String, count: Int) -> String { "${name}" } describe(count: 3, name: "widget") -- all named, reordered freely describe("widget", count: 3) -- positional first, then named ``` Named arguments work for calls to functions in the same module or agent, and for effect calls with documented signatures (`llm.chat(model:, system:, input:)`, `memory.put(key:, value:)`, `http.post(url:, json:)`, and so on). The analyzer resolves names against the callee’s declared parameter names and rewrites the call into positional order at compile time, so there is no runtime cost — codegen only ever sees positional arguments. These are compile errors (`E0026`, with a `fix_hint` listing the valid names): * An unknown or duplicate argument name * A positional argument after a named one * Naming a parameter that was already filled positionally * Named arguments on a callee without a known signature (e.g. stdlib calls) Patterns never use named arguments — `Event.Charge(amount: 5)` in a `match` arm is a parse error. ## Variable Naming Convention [Section titled “Variable Naming Convention”](#variable-naming-convention) Skein uses `snake_case` for variable names. Core Erlang requires capitalized variable names. The compiler transforms variable names: | Skein | Core Erlang | | ----------- | ----------- | | `x` | `X` | | `name` | `Name` | | `my_var` | `MyVar` | | `ticket_id` | `TicketId` | The transformation splits on underscores, capitalizes each segment, and joins them. ## Unique Variables [Section titled “Unique Variables”](#unique-variables) The code generator needs temporary variables for intermediate results (e.g., match subjects). These are generated using a process-local counter: ```elixir # Generated names: _skein_0, _skein_1, _skein_2, ... defp gen_var do counter = Process.get(:skein_var_counter, 0) Process.put(:skein_var_counter, counter + 1) String.to_atom("_skein_#{counter}") end ``` These variables are invisible to the Skein programmer. # Handlers > HTTP, queue, and schedule handler syntax for building services in Skein. ## Overview [Section titled “Overview”](#overview) Handlers are the primary way Skein programs receive external input. They declare how a module responds to HTTP requests, queue messages, and scheduled events. Each handler type requires a corresponding capability declaration. ## Handler Types [Section titled “Handler Types”](#handler-types) Skein supports four handler types: | Type | Capability | Trigger | Example | | -------- | ------------------ | ------------------------------------ | ----------------------------------------------- | | HTTP | `http.in` | Incoming HTTP request | `handler http GET "/users" (req) -> { ... }` | | Queue | `queue.consume` | Message from a named queue | `handler queue "events" (msg) -> { ... }` | | Topic | `topic.consume` | Broadcast message from a named topic | `handler topic "order.events" (msg) -> { ... }` | | Schedule | `schedule.trigger` | Cron-triggered timer | `handler schedule "*/5 * * * *" () -> { ... }` | ## HTTP Handlers [Section titled “HTTP Handlers”](#http-handlers) HTTP handlers respond to incoming HTTP requests. They specify an HTTP method, a route pattern, and receive a request map. ### Syntax [Section titled “Syntax”](#syntax) ```skein handler http METHOD "route" (param) -> { -- body } ``` * **METHOD**: One of `GET`, `POST`, `PUT`, `PATCH`, `DELETE` * **route**: A string path, optionally with `:param` segments for path parameters * **param**: The name bound to the request map ### Request Map [Section titled “Request Map”](#request-map) The handler parameter receives a map with: | Field | Type | Description | | --------- | ------ | ---------------------------------------- | | `params` | Map | Path parameters extracted from the route | | `headers` | Map | Request headers | | `body` | String | Raw request body | | `method` | Atom | HTTP method (`:get`, `:post`, etc.) | | `path` | String | Request path | ### Example [Section titled “Example”](#example) ```skein module UserService { capability http.in handler http GET "/users/:id" (req) -> { let id = req.params.id respond.json(200, id) } handler http POST "/users" (req) -> { respond.json(201, "created") } } ``` ### Request Body Validation [Section titled “Request Body Validation”](#request-body-validation) Use `req.json[T]` to parse and validate JSON request bodies against a declared type: ```skein module OrderService { capability http.in type CreateOrder { product_id: String quantity: Int @min(1) } handler http POST "/orders" (req) -> { let order = req.json[CreateOrder] respond.json(201, "ok") } } ``` ## Queue Handlers [Section titled “Queue Handlers”](#queue-handlers) Queue handlers process messages from named message queues. They are triggered asynchronously when a message is published to the subscribed queue. ### Syntax [Section titled “Syntax”](#syntax-1) ```skein handler queue "queue-name" (param) -> { -- body } ``` * **queue-name**: A string identifying the queue to subscribe to * **param**: The name bound to the message map ### Example [Section titled “Example”](#example-1) ```skein module OrderWorker { capability queue.consume handler queue "order-events" (msg) -> { let data = msg respond.json(200, "processed") } handler queue "priority-events" (msg) -> { respond.json(200, "priority-processed") } } ``` ### Runtime Behavior [Section titled “Runtime Behavior”](#runtime-behavior) At startup, queue handlers are registered with `Skein.Runtime.Queue`. Messages published to the queue are dispatched to the handler function asynchronously. Messages are delivered in order within a single queue. ## Schedule Handlers [Section titled “Schedule Handlers”](#schedule-handlers) Schedule handlers run on a cron-based schedule. They take no input parameters and execute periodically. ### Syntax [Section titled “Syntax”](#syntax-2) ```skein handler schedule "cron-expression" () -> { -- body } ``` * **cron-expression**: A standard 5-field cron expression (`minute hour day month weekday`) ### Cron Expression Examples [Section titled “Cron Expression Examples”](#cron-expression-examples) | Expression | Meaning | | ------------- | -------------------------------- | | `*/5 * * * *` | Every 5 minutes | | `0 * * * *` | Every hour on the hour | | `0 0 * * *` | Daily at midnight | | `0 9 * * 1` | Every Monday at 9:00 AM | | `30 8 1 * *` | 8:30 AM on the 1st of each month | ### Example [Section titled “Example”](#example-2) ```skein module Maintenance { capability schedule.trigger handler schedule "*/5 * * * *" () -> { respond.json(200, "cleanup") } handler schedule "0 0 * * *" () -> { respond.json(200, "daily-report") } } ``` ### Runtime Behavior [Section titled “Runtime Behavior”](#runtime-behavior-1) At startup, schedule handlers are registered with `Skein.Runtime.Schedule`. The runtime triggers handlers at the appropriate times. For testing, handlers can be triggered manually with `Skein.Runtime.Schedule.trigger/1`. ## Topic Handlers [Section titled “Topic Handlers”](#topic-handlers) Topic handlers subscribe to named topics and receive every message published to that topic. Unlike queue handlers (which deliver each message to a single consumer), topic handlers use **fan-out semantics**: every subscriber receives every message. ### Syntax [Section titled “Syntax”](#syntax-3) ```skein handler topic "topic-name" (param) -> { -- body } ``` * **topic-name**: A string identifier for the topic (e.g., `"order.events"`) * **param**: The message parameter name, bound to the published data ### Example [Section titled “Example”](#example-3) ```skein module PubsubNotifications { capability http.in capability topic.consume("order.events") capability topic.publish("order.events") -- Publish side: an HTTP endpoint fans an event out to every consumer handler http POST "/orders" (req) -> { topic.publish("order.events", { order_id: "o-1" }) respond.json(202, "queued") } -- Email notification: receives every order event handler topic "order.events" (msg) -> { let event = msg respond.json(200, "email-sent") } -- Analytics: also receives every order event (fan-out) handler topic "order.events" (msg) -> { let event = msg respond.json(200, "analytics-recorded") } } ``` ### Topic vs Queue [Section titled “Topic vs Queue”](#topic-vs-queue) | | Queue | Topic | | ---------- | --------------------------- | --------------------------- | | Delivery | Single consumer | All subscribers (fan-out) | | Capability | `queue.consume` | `topic.consume` | | Use case | Job processing | Notifications, events | | Publish | `queue.publish(name, data)` | `topic.publish(name, data)` | ### Runtime Behavior [Section titled “Runtime Behavior”](#runtime-behavior-2) At startup, topic handlers are registered with `Skein.Runtime.Topic`. Messages published via `topic.publish(name, data)` are dispatched asynchronously to ALL subscribers. Messages are delivered in order within each subscriber. ## Mixed Handler Types [Section titled “Mixed Handler Types”](#mixed-handler-types) A module can declare handlers of multiple types. Each type requires its own capability: ```skein module QueueWorker { capability http.in capability queue.consume capability schedule.trigger handler http GET "/health" (req) -> { respond.json(200, "ok") } handler queue "jobs" (msg) -> { respond.json(200, "processed") } handler schedule "*/5 * * * *" () -> { respond.json(200, "cleanup") } } ``` ## Compilation [Section titled “Compilation”](#compilation) All handler types compile to the same pattern: 1. Each handler becomes a `__handler_N__/1` function in the compiled module 2. Handler metadata is returned by `__handlers__/0` with fields: `source`, `method`, `route`, `handler` 3. The `source` field distinguishes handler types: `:http`, `:queue`, `:topic`, `:schedule` ```elixir # Example __handlers__/0 output for mixed types [ %{source: :http, method: :get, route: "/health", handler: :__handler_0__}, %{source: :queue, method: nil, route: "jobs", handler: :__handler_1__}, %{source: :schedule, method: nil, route: "*/5 * * * *", handler: :__handler_2__} ] ``` ## Response Helpers [Section titled “Response Helpers”](#response-helpers) Handlers return responses using one of three built-in helpers. Each sets the appropriate HTTP `Content-Type` header automatically. | Helper | Content-Type | Use When | | ---------------------------- | ------------------ | ---------------------------------------------------- | | `respond.json(status, body)` | `application/json` | Returning structured data (maps, lists, values) | | `respond.text(status, body)` | `text/plain` | Returning plain text (health checks, simple strings) | | `respond.html(status, body)` | `text/html` | Returning HTML pages or fragments | ### Examples [Section titled “Examples”](#examples) ```skein module HelloHttp { capability http.in -- JSON API endpoint handler http GET "/api/users" (req) -> { respond.json(200, "users") } -- Plain text health check handler http GET "/health" (req) -> { respond.text(200, "ok") } -- HTML page handler http GET "/page" (req) -> { respond.html(200, "

Hello from Skein

") } } ``` All three helpers take two arguments: * **status**: An integer HTTP status code (e.g., `200`, `201`, `404`, `500`) * **body**: A string value to send as the response body For `respond.json`, the body is JSON-encoded before sending. For `respond.text` and `respond.html`, the body string is sent as-is. ## Idempotent Handlers [Section titled “Idempotent Handlers”](#idempotent-handlers) The `idempotent(key)` guard prevents duplicate processing of messages. Place it at the top of a handler body — if the key has already been processed, the handler skips execution silently. ```skein module BillingWorker { capability queue.consume handler queue "billing.events" (msg) -> { idempotent(msg.id) -- This code only runs once per unique msg.id let amount = msg.body respond.json(200, amount) } } ``` **How it works:** 1. On first call with a given key, `idempotent()` records the key and continues execution 2. On subsequent calls with the same key, the handler is skipped entirely 3. Keys expire after a configurable TTL (default: 1 hour), allowing reprocessing after expiry **Constraints:** * `idempotent()` can only be used inside handler bodies (not in regular functions or agent handlers). Using it elsewhere produces error `E0035`. * The key argument must be a `String` expression. **Dispatch behavior on skip:** | Handler Type | Skip Response | | ------------ | --------------------------------- | | HTTP | Returns `200 "already processed"` | | Queue | Silently drops the message | | Topic | Silently drops the message | | Schedule | Silently skips the invocation | ## Capability Requirements [Section titled “Capability Requirements”](#capability-requirements) The analyzer checks that each handler type has its required capability declared. Missing capabilities produce error `E0012`: ```plaintext Error E0012: Capability 'queue.consume' required but not declared. Queue handlers require this capability. Fix: capability queue.consume ``` # Modules and Functions > How Skein modules and functions work, and how they compile to BEAM modules. ## Module Declaration [Section titled “Module Declaration”](#module-declaration) A Skein file contains a single module declaration: ```skein module Hello { fn greet(name: String) -> String { "Hello, ${name}!" } } ``` ## Module Naming and Elixir Interop [Section titled “Module Naming and Elixir Interop”](#module-naming-and-elixir-interop) Skein modules compile to BEAM modules with Elixir-compatible names: | Skein Name | BEAM Module Name | | ------------- | ------------------------------- | | `Hello` | `Elixir.Skein.User.Hello` | | `UserService` | `Elixir.Skein.User.UserService` | | `Math` | `Elixir.Skein.User.Math` | The `Elixir.Skein.User.` prefix: * Prevents naming collisions with Elixir/Erlang standard modules * Enables natural Elixir interop: `Skein.User.Hello.greet("World")` * Follows Elixir’s `Elixir.` prefix convention for BEAM module atoms ## `__info__/1` for Elixir Compatibility [Section titled “\_\_info\_\_/1 for Elixir Compatibility”](#__info__1-for-elixir-compatibility) Every compiled module includes an `__info__/1` function that makes it behave like an Elixir module: ```elixir mod.__info__(:module) #=> Elixir.Skein.User.Hello mod.__info__(:functions) #=> [greet: 1, add: 2, classify: 1] ``` This is generated automatically by the code generator. It supports the `:module` and `:functions` queries that Elixir tooling expects. ## Function Declarations [Section titled “Function Declarations”](#function-declarations) Functions require: * A name (lowercase identifier) * Typed parameters (may be empty) * A return type * A body block ```skein fn add(a: Int, b: Int) -> Int { a + b } fn get_version() -> String { "0.1.0" } ``` ### Parameters [Section titled “Parameters”](#parameters) Parameters use `name: Type` syntax, separated by commas: ```skein fn create_user(name: String, email: String, age: Int) -> String { "Created ${name}" } ``` Each parameter compiles to a Core Erlang function variable. The parameter name is transformed from snake\_case to CamelCase (e.g., `first_name` becomes `FirstName`). ### Return Type [Section titled “Return Type”](#return-type) The return type follows the `->` arrow. It is required on all functions: ```skein fn compute(x: Int) -> Int { ... } fn greet(name: String) -> String { ... } fn is_valid(n: Int) -> Bool { ... } ``` ### Body [Section titled “Body”](#body) The function body is always a block (`{ ... }`). The last expression is the return value: ```skein fn process(x: Int) -> Int { let doubled = x * 2 -- intermediate binding let adjusted = doubled + 1 adjusted -- this is the return value } ``` ## Multi-Function Modules [Section titled “Multi-Function Modules”](#multi-function-modules) Modules typically contain multiple functions: ```skein module Calculator { fn add(a: Int, b: Int) -> Int { a + b } fn subtract(a: Int, b: Int) -> Int { a - b } fn multiply(a: Int, b: Int) -> Int { a * b } fn is_positive(n: Int) -> Bool { n > 0 } } ``` All functions are exported from the compiled BEAM module for Elixir interop and runtime dispatch. There is no visibility modifier — but note that from *Skein* code, functions are module-private: another Skein module cannot call them (see Module Boundaries below). ## Module Boundaries [Section titled “Module Boundaries”](#module-boundaries) **Tools are the only cross-module seam.** A module’s functions, types, and enums are private to that module. There is no `import`, no `use`, and no qualified cross-module function call — `OtherModule.fn_name(args)` is not Skein. To invoke code in another module, that module must expose a tool, and the caller must declare the grant: ```skein -- Module A: declare the grant, then call capability tool.use(Billing.CreateRefund) let result = tool.call(Billing.CreateRefund, { ticket_id: id }) ``` Dotted calls with an uppercase head (`String.upcase(s)`) are reserved for the standard library, which is ambient — available everywhere with no declaration. This is a deliberate design decision: tool boundaries keep capability grants explicit at the call site, validate every cross-module payload against a schema, and produce a trace span for every call. The full rationale lives in the spec’s [Module Boundaries section](https://github.com/kormie/Skein/blob/main/docs/SKEIN_SPEC.md#module-boundaries-tools-are-the-only-cross-module-seam). Unit tests are co-located in the module body (`test`, `scenario`, `golden` declarations), where the functions under test are in scope. Cross-module integration tests go through `tool.call`, exactly as production callers do. ## How Functions Compile [Section titled “How Functions Compile”](#how-functions-compile) A Skein function: ```skein fn add(a: Int, b: Int) -> Int { a + b } ``` Compiles to a Core Erlang function definition: ```erlang 'add'/2 = fun (A, B) -> call 'erlang':'+'(A, B) ``` The code generator: 1. Creates a Core Erlang function name via `:cerl.c_fname(name_atom, arity)` 2. Creates parameter variables with CamelCase names via `:cerl.c_var/1` 3. Generates the body expression recursively 4. Wraps it all in `:cerl.c_fun/2` 5. Adds the function to the module’s export list and definition list ## Compilation Pipeline for a Module [Section titled “Compilation Pipeline for a Module”](#compilation-pipeline-for-a-module) ```plaintext Source text | v Lexer: tokens = [{:module, {1,1}}, {:upper_ident, {1,8}, "Hello"}, {:lbrace, {1,14}}, ...] | v Parser: %AST.Module{name: "Hello", declarations: [%AST.Fn{...}], meta: %{...}} | v Analyzer: type, capability, transition, and guard checking; rewrites named arguments to positional | v CodeGen: :cerl.c_module(...) -> :compile.forms(mod, [:from_core, :binary]) -> beam_binary | v Load: :code.load_binary(module_atom, ~c"nofile", beam_binary) -> {:module, module} ``` # Supervisors > Declaring supervision trees for agent pools and crash recovery in Skein. ## Overview [Section titled “Overview”](#overview) Supervisor declarations describe groups of child agents and the crash-recovery strategies applied to them, following the standard OTP supervision model. You declare supervisors alongside your other module-level constructs. Supervisor declarations are **real supervision** ([#325](https://github.com/kormie/Skein/issues/325)): the compiler validates them (see [Analyzer Validation](#analyzer-validation)) and emits the tree as [`__supervisors__/0` metadata](#runtime-metadata), and `skein run` boots one OTP supervisor per declaration for as long as the service runs. A crashed child is restarted per the declared strategy, every (re)start appends a `:supervisor`/`:child_started` event to the event log, and `memory.kv` data survives child restarts — the store outlives agent processes. ## Syntax [Section titled “Syntax”](#syntax) ```plaintext supervisor Name { child Target child Target(Arg) { key: value } strategy: one_for_one | one_for_all | rest_for_one max_restarts: count per period_seconds } ``` All directives are optional except that a supervisor should have at least one `child` (the analyzer will warn otherwise). ## Children [Section titled “Children”](#children) Each `child` directive names an agent in the same module to supervise: ```skein module MyService { supervisor Main { child Worker } agent Worker { enum Phase { Waiting -> [] } on start() -> { transition(Phase.Waiting) } on phase(Phase.Waiting) -> { 0 } } } ``` At boot, each `child` target resolves to the module’s compiled nested agent (`Skein.Agent..`). A child whose target does not resolve to an agent in the module is skipped with a `:supervisor`/`:child_skipped` event rather than failing the boot. ### Child Arguments [Section titled “Child Arguments”](#child-arguments) The named entries in a child’s `{ key: value }` block (other than `restart:`) are the agent’s start arguments — they become the argument map passed to the agent’s `on start(...)` handler: ```skein supervisor Pool { child Worker { n: 5 } } ``` starts `Worker` with `n = 5` in its `on start(n: Int)` handler. Parenthesized arguments (`child AgentPool(RefundAgent)`) distinguish child declarations but are not passed to the agent’s start handler. ### Restart Policies [Section titled “Restart Policies”](#restart-policies) The `restart:` option in a child’s block selects the OTP restart policy: ```skein supervisor Main { child Worker { restart: permanent } child BatchWorker { restart: transient } child OneShot { restart: temporary } } ``` | Policy | Behavior | | ----------- | -------------------------------------------- | | `permanent` | Always restarted when it exits (the default) | | `transient` | Restarted only on abnormal exit | | `temporary` | Never restarted | ## Strategy [Section titled “Strategy”](#strategy) The `strategy:` directive declares how sibling children are affected when one crashes: ```skein supervisor Main { child Worker child Watcher strategy: one_for_one } ``` | Strategy | Behavior | | -------------- | ----------------------------------------------------------------- | | `one_for_one` | Only the crashed child is restarted | | `one_for_all` | All children are restarted when one crashes | | `rest_for_one` | The crashed child and all children started after it are restarted | If omitted, `one_for_one` is the default. ## Max Restarts [Section titled “Max Restarts”](#max-restarts) The `max_restarts:` directive is the OTP crash intensity limit: more than `count` restarts within `period` seconds shuts down the supervisor itself. ```skein supervisor Main { child Worker max_restarts: 10 per 60s } ``` This means: at most 10 restarts in any 60-second window. When omitted, the OTP defaults apply. ## Restarts in the Trace [Section titled “Restarts in the Trace”](#restarts-in-the-trace) Every child start appends a `:supervisor` event to the unified event log — and because restarts re-run the child start, restart #2+ show up as additional `:child_started` events: ```elixir Skein.Runtime.EventStore.query(kind: :supervisor, event: :child_started) # => one event per start, including each restart: # %{kind: :supervisor, event: :child_started, supervisor: "Main", child: "Worker", ...} ``` Agent `memory.kv` writes survive these restarts: the memory table is owned by the supervised runtime, not the agent process, so data written before a crash is still readable after the supervisor brings the child back. ## Full Example [Section titled “Full Example”](#full-example) ```skein module RefundService { supervisor Main { child RefundWorker { restart: permanent } strategy: one_for_one max_restarts: 10 per 60s } agent RefundWorker { capability memory.kv enum Phase { Waiting -> [] } on start() -> { memory.put("started", "true") transition(Phase.Waiting) } on phase(Phase.Waiting) -> { 0 } } } ``` ## Runtime Metadata [Section titled “Runtime Metadata”](#runtime-metadata) The `__supervisors__/0` metadata function is the supervisor declaration’s compiled artifact — the [stability policy](/Skein/reference/stability/) classifies it as compiled-module metadata. It is what `skein run` (via `Skein.Runtime.SupervisorHost`) realizes as OTP supervision. Modules with supervisors expose it, returning the supervisor tree: ```elixir mod.__supervisors__() # => [ # %{ # name: "Main", # strategy: :one_for_one, # max_restarts: {10, 60}, # children: [ # %{target: "RefundWorker", args: [], options: %{restart: "permanent"}} # ] # } # ] ``` ## Analyzer Validation [Section titled “Analyzer Validation”](#analyzer-validation) The analyzer checks supervisor declarations and produces errors/warnings: | Code | Severity | Description | | ----- | -------- | ---------------------------- | | E0040 | error | Invalid strategy value | | E0041 | error | Invalid max\_restarts format | | E0042 | warning | Supervisor has no children | # Syntax Overview > The current Skein syntax -- what's implemented and how it works. ## Universal Pattern [Section titled “Universal Pattern”](#universal-pattern) Every Skein construct follows the same structural pattern: ```plaintext ? ``` This regularity is a deliberate design choice for agent-writability — an LLM that learns the pattern once can generate any construct. ## Modules [Section titled “Modules”](#modules) Modules are the top-level organizational unit. Every Skein file contains a single module: ```skein module Hello { fn greet(name: String) -> String { "Hello, ${name}!" } } ``` Module names must start with an uppercase letter (`[A-Z][a-zA-Z0-9_]*`). ## Functions [Section titled “Functions”](#functions) Functions use `fn`. They are always named (no anonymous lambdas). Parameters have explicit types, and a return type is required. ```skein fn calculate(a: Int, b: Int) -> Int { let sum = a + b sum * 2 } ``` The last expression in the block is the return value. There is no `return` keyword. Zero-parameter functions omit the parens content: ```skein fn get_version() -> String { "0.1.0" } ``` ## Let Bindings [Section titled “Let Bindings”](#let-bindings) All bindings use `let`. Bindings are immutable — there is no `mut`, `var`, or reassignment. ```skein fn example(x: Int) -> Int { let doubled = x + x let result = doubled * 3 result } ``` ## Match Expressions [Section titled “Match Expressions”](#match-expressions) `match` is the **only** conditional construct in Skein. No `if/else`, no ternary, no `cond`. ```skein fn classify(n: Int) -> String { match n > 0 { true -> "positive" false -> "non-positive" } } ``` Match arms use the `pattern -> expression` syntax. Each arm body can be a single expression or a block: ```skein match status { "active" -> handle_active() "paused" -> { let reason = get_reason() handle_paused(reason) } } ``` ## Pipe Operator [Section titled “Pipe Operator”](#pipe-operator) The pipe `|>` threads the result of the left side as the first argument of the right side: ```skein fn process(data: String) -> String { data |> transform() |> validate() |> format() } ``` ## String Interpolation [Section titled “String Interpolation”](#string-interpolation) Strings use double quotes with `${}` for interpolation: ```skein fn greet(name: String) -> String { "Hello, ${name}!" } ``` Plain strings without interpolation are also supported: ```skein fn label() -> String { "no interpolation here" } ``` ## Comments [Section titled “Comments”](#comments) Comments use `--` (double dash). There are no block comments. ```skein -- This is a comment fn add(a: Int, b: Int) -> Int { a + b -- inline comment } ``` ## Operators [Section titled “Operators”](#operators) ### Arithmetic [Section titled “Arithmetic”](#arithmetic) | Operator | Meaning | | ------------ | --------------------- | | `+` | Addition | | `-` | Subtraction | | `*` | Multiplication | | `/` | Division | | `-` (prefix) | Negation (`-3`, `-x`) | There is no negative-literal token: negative numbers are written with prefix `-`, which requires an `Int` or `Float` operand and preserves its type. ### Comparison [Section titled “Comparison”](#comparison) | Operator | Meaning | | -------- | --------------------- | | `==` | Equal | | `!=` | Not equal | | `<` | Less than | | `>` | Greater than | | `<=` | Less than or equal | | `>=` | Greater than or equal | ### Logical [Section titled “Logical”](#logical) | Operator | Meaning | | ------------ | ----------- | | `&&` | Logical AND | | `\|\|` | Logical OR | | `!` (prefix) | Logical NOT | ### Postfix [Section titled “Postfix”](#postfix) | Operator | Meaning | | ------------- | ------------------------------ | | `!` (postfix) | Unwrap Result (crash on error) | | `?` (postfix) | Propagate Result error | ### Precedence (lowest to highest) [Section titled “Precedence (lowest to highest)”](#precedence-lowest-to-highest) | Level | Operators | Associativity | | ----- | -------------------- | ------------- | | 1 | `\|>` | Left | | 2 | `\|\|` | Left | | 3 | `&&` | Left | | 4 | `==`, `!=` | Left | | 5 | `<`, `>`, `<=`, `>=` | Left | | 6 | `+`, `-` | Left | | 7 | `*`, `/` | Left | | 8 | `!`, `-` (prefix) | Prefix | | 9 | `!`, `?` (postfix) | Postfix | | 10 | `.` (field access) | Left | | 11 | Function call `f(x)` | — | ## Function Calls and Field Access [Section titled “Function Calls and Field Access”](#function-calls-and-field-access) ```skein -- Function call let result = compute(x, y) -- Field access let name = user.name -- Chained let city = user.address.city -- Function reference (for higher-order use) let ref = &my_function ``` ## Blocks [Section titled “Blocks”](#blocks) Blocks use braces. Always. No significant whitespace, no optional braces. ```skein { let x = compute(y) x + 1 } ``` The last expression in a block is its return value. ## Agents [Section titled “Agents”](#agents) Agents are state machines with phases, transitions, and event-driven handlers: ```skein agent RefundBot { capability memory.kv("sessions") capability model("anthropic", "claude-opus-4-8") state { request_id: String amount: Int } enum Phase { Review -> [Approved, Denied] Approved -> [Done] Denied -> [Done] Done -> [] } on start(request_id: String, amount: Int) -> { memory.put("request_id", request_id) memory.put("amount", amount) transition(Phase.Review) } on phase(Phase.Review) -> { let request_id = memory.get("request_id")! let decision = llm.chat("claude-opus-4-8", "Evaluate this refund", request_id)! match decision { "approve" -> transition(Phase.Approved) _ -> transition(Phase.Denied) } } on phase(Phase.Approved) -> { let amount = memory.get("amount")! emit RefundApproved { amount: amount } transition(Phase.Done) } on phase(Phase.Denied) -> { transition(Phase.Done) } on phase(Phase.Done) -> { stop() } } ``` Key features: * `Phase` enum with `-> [Target, ...]` transition declarations — validated at compile time; every phase needs a handler * `on start(name: Type, ...) -> { ... }` handler with typed parameters runs when the agent starts * `on phase(Phase.X) -> { ... }` handlers run when entering each phase * `transition(Phase.X)` moves to a new phase (invalid transitions are compiler errors) * `state.field` reads agent state, which starts **empty** at runtime — start parameters are NOT copied into state, and a `state.field` read only sees what a handler has explicitly returned as state. The durable pattern (shown above) is `memory.put(...)` in `on start` and `memory.get(...)!` in phase handlers, which requires a `capability memory.kv(...)` declaration * `emit EventName { field: value }` for domain events * `stop()` to terminate the agent ## Implementation Status [Section titled “Implementation Status”](#implementation-status) Most Skein constructs are fully compiled end-to-end. The following notes clarify the status of specific features: ### Fully Compiled [Section titled “Fully Compiled”](#fully-compiled) * `tool` declarations — tool metadata (name, description, input/output schemas) is compiled and available at runtime via `__tools__/0`. See [Tools](/Skein/language/tools/) for details. * `test`, `scenario`, `golden` — all three test constructs compile to executable `__test_N__/0` functions with `__tests__/0` metadata. See [Testing](/Skein/language/testing/) for details. ### Also Compiled [Section titled “Also Compiled”](#also-compiled) * `supervisor` declarations — compile-time validated supervision trees (children, restart policies, strategy, restart limits) emitted as `__supervisors__/0` metadata for a host to materialize. See [Supervisors](/Skein/language/supervisors/) for details. * Enum variant pattern matching — `match` on enum variants with field destructuring compiles to BEAM tuple patterns. See [Types > Enum Variant Matching](/Skein/language/types/#enum-variant-matching). # Testing > Built-in test constructs in the Skein language for inline testing of modules. ## Overview [Section titled “Overview”](#overview) Skein has built-in test declarations that live alongside the code they test. Tests are compiled into the module as executable functions and can be run via the CLI. ## Test Declarations [Section titled “Test Declarations”](#test-declarations) Use the `test` keyword inside a module to declare a test: ```skein module Math { fn add(a: Int, b: Int) -> Int { a + b } fn double(x: Int) -> Int { x * 2 } test "add returns correct sum" { assert add(2, 3) == 5 assert add(0, 0) == 0 assert add(-1, 1) == 0 } test "double works" { assert double(1) == 2 assert double(0) == 0 } } ``` ### Syntax [Section titled “Syntax”](#syntax) ```plaintext test "" { } ``` The body can contain any expression, including `let` bindings, `match` expressions, function calls, and `assert` statements. ## Assert [Section titled “Assert”](#assert) The `assert` keyword evaluates an expression and raises a structured `Skein.Runtime.AssertionError` if the result is not `true`: ```skein assert add(2, 3) == 5 -- passes: 2 + 3 == 5 is true assert add(2, 3) == 99 -- fails: raises Skein.Runtime.AssertionError ``` The error carries the comparison operator and both operand values (`op`, `left`, `right`), the rendered source expression (`expr`), and the assert’s source location (`file`, `line`) — so failures report expected vs actual instead of a bare “Assertion failed”. Assert works with any expression that returns a boolean: ```skein test "comparison operators" { assert 5 > 3 assert 1 == 1 assert 10 != 5 } ``` ## How Tests Compile [Section titled “How Tests Compile”](#how-tests-compile) Each `test` declaration compiles to: 1. A **test function** `__test_N__/0` that executes the body and returns `:ok` on success (or raises on assertion failure) 2. An entry in the **test metadata** function `__tests__/0` ```elixir mod.__tests__() #=> [ # %{description: "add returns correct sum", fn: :__test_0__}, # %{description: "double works", fn: :__test_1__} # ] mod.__test_0__() #=> :ok (or raises Skein.Runtime.AssertionError) ``` ## Running Tests [Section titled “Running Tests”](#running-tests) ### Project-Wide [Section titled “Project-Wide”](#project-wide) ```bash # Discover and run all tests across src/ and test/ directories skein test my_project # Or via Mix: mix skein.test my_project ``` (`mix skein.compile` only compiles a file — it never runs tests.) The test runner: 1. Walks `test/` and `src/` directories for `.skein` files 2. Compiles each file (skipping files with compile errors) 3. Discovers test declarations via `__tests__/0` 4. Runs each test function and reports aggregate results ```plaintext Tests: 4 passed, 1 failed (5 total) FAIL: wrong assertion (src/math.skein:12) — Assertion failed: expected 5, got 4 ``` Passing tests print no per-test lines; failures go to stderr with their location and error, files that fail to compile are listed as `N file(s) failed to compile and were not tested:` followed by their diagnostics, and the exit code is non-zero when anything failed. ### Programmatically [Section titled “Programmatically”](#programmatically) ```elixir # Single file {:ok, mod, _warnings} = Skein.CLI.compile(["path/to/module.skein"]) {:ok, results} = Skein.CLI.test(["path/to/module.skein"]) results.total #=> 2 results.passed #=> 2 results.failed #=> 0 # Project-wide {:ok, results} = Skein.CLI.test_all(["my_project"]) results.total #=> 5 results.passed #=> 4 results.failed #=> 1 results.files #=> 3 results.compile_errors #=> 0 ``` ## Tests Coexist with Code [Section titled “Tests Coexist with Code”](#tests-coexist-with-code) Tests are regular module declarations. They can be mixed freely with functions, types, capabilities, and handlers: ```skein module UserService { type User { name: String email: Email } fn format_name(first: String, last: String) -> String { "${first} ${last}" } test "format_name joins names" { assert format_name("Jane", "Doe") == "Jane Doe" } } ``` ## Scenario Tests [Section titled “Scenario Tests”](#scenario-tests) Scenario tests provide structured BDD-style testing with explicit `given`/`expect` blocks. The `given` block establishes variable bindings, and the `expect` block contains assertions that run with those bindings in scope. ### Syntax [Section titled “Syntax”](#syntax-1) ```plaintext scenario "" { given { : ... } expect { assert ... } } ``` ### Example [Section titled “Example”](#example) ```skein module RefundService { fn calculate_refund(amount: Int, rate: Int) -> Int { amount * rate } scenario "high-value refund calculation" { given { amount: 50000 rate: 2 } expect { assert calculate_refund(amount, rate) == 100000 assert amount > 10000 } } } ``` The `given` variables (`amount`, `rate`) are accessible in the `expect` block. The scenario compiles to a test function just like `test` — it appears in `__tests__/0` with `kind: :scenario`. ### When to Use Scenarios [Section titled “When to Use Scenarios”](#when-to-use-scenarios) * Testing with explicit, named inputs for readability * BDD-style specifications where setup and assertions are cleanly separated * Agent behavior testing with structured initial state ## Golden Tests [Section titled “Golden Tests”](#golden-tests) Golden tests load a recorded trace file and run assertions against it. They are used for regression testing — recording a known-good execution and verifying it still produces the expected outcomes. ### Syntax [Section titled “Syntax”](#syntax-2) ```plaintext golden "" from trace "" { assert ... } ``` ### Example [Section titled “Example”](#example-1) ```skein module ApiTests { fn ok() -> Bool { true } golden "refund flow trace" from trace "traces/refund_001.json" { assert ok() } } ``` The trace file must be a JSON array of span objects. At test execution time, the trace is loaded via `Skein.Runtime.Replay.load_trace/1` before the body runs. Golden tests appear in `__tests__/0` with `kind: :golden`. ### Trace File Format [Section titled “Trace File Format”](#trace-file-format) Trace files contain event objects from the unified event store (`Skein.Runtime.EventStore`). Each event has at minimum a `kind` field: ```json [ {"kind": "effect", "method": "get", "url": "https://api.example.com/refund", "status": 200}, {"kind": "annotation", "key": "decision", "value": "approved"}, {"kind": "state_change", "namespace": "sessions", "operation": "put", "key": "decision", "value": "approved"}, {"kind": "user_event", "event": "refund.approved", "data": {"amount": 100}} ] ``` Supported event kinds: `effect`, `state_change`, `user_event`, `annotation`. Legacy span kinds (`handler`, `llm`, `memory`, `http`) are still accepted for traces recorded before the unified event store. ## Test Kind Field [Section titled “Test Kind Field”](#test-kind-field) All three test forms (`test`, `scenario`, `golden`) compile to `__test_N__/0` functions and appear in `__tests__/0` metadata. Each entry includes a `:kind` field: ```elixir mod.__tests__() #=> [ # %{description: "unit test", fn: :__test_0__, kind: :test}, # %{description: "scenario", fn: :__test_1__, kind: :scenario}, # %{description: "trace check", fn: :__test_2__, kind: :golden} # ] ``` The CLI test runner uses this field to report which type of test passed or failed. # Tools > How to declare and call tools in Skein for LLM function-calling and external integrations. ## Overview [Section titled “Overview”](#overview) Tools are Skein’s mechanism for **LLM function calling** and structured external integrations. A tool declaration defines a contract (input schema, output schema) and an implementation. At compile time, the compiler generates JSON Schema manifests that LLM providers can use for function-calling. ## Declaring Tools [Section titled “Declaring Tools”](#declaring-tools) Tools are declared inside modules with the `tool` keyword: ```skein module PaymentService { tool CreateRefund { description: "Issue a refund via the payment provider" input { customer_id: String @description("Stripe customer ID") amount: Int @min(1) @max(100000) } output { id: String status: String } errors { RefundError } implement { match http.post("https://api.stripe.com/v1/refunds", customer_id) { Ok(r) -> Ok({ id: r.body.id, status: r.body.status }) Err(e) -> Err(RefundError.from(e)) } } } } ``` ### Syntax [Section titled “Syntax”](#syntax) ```plaintext tool { description: "" -- optional input { -- required: input fields : [annotations] } output { -- required: output fields : } errors { , ... } -- optional: error types implement { -- required: implementation body } } ``` The `implement` body must evaluate to `Result[output, error]` — the runtime invokes it and matches on `Ok`/`Err`, so a bare value is a compile error (`E0020`). `Ok({ ... })` payloads are checked field-by-field against the declared `output { ... }` shape. ### Dotted Names [Section titled “Dotted Names”](#dotted-names) Tool names can use dots for namespacing: ```skein tool Stripe.CreateRefund { ... } tool Stripe.GetBalance { ... } ``` ## JSON Schema Generation [Section titled “JSON Schema Generation”](#json-schema-generation) The compiler automatically generates JSON Schema from tool input and output fields. This is available at runtime via the `__tools__/0` metadata function. Given: ```skein tool CreateRefund { input { customer_id: String @description("Stripe customer ID") amount: Int @min(1) @max(100000) } output { id: String, status: String } implement { Ok({ id: "r1", status: "ok" }) } } ``` The generated `input_schema` is: ```json { "type": "object", "properties": { "customer_id": { "type": "string", "description": "Stripe customer ID" }, "amount": { "type": "integer", "minimum": 1, "maximum": 100000 } }, "required": ["amount", "customer_id"] } ``` Constraint annotations (`@min`, `@max`, `@description`, `@one_of`, `@default`) are included in the schema. `Option[T]` fields are excluded from `required`. ## Tool Metadata [Section titled “Tool Metadata”](#tool-metadata) Every compiled module with tools exports `__tools__/0`: ```elixir mod.__tools__() #=> [%{ # name: "CreateRefund", # description: "Issue a refund via the payment provider", # input: [%{name: "customer_id", type: "String"}, %{name: "amount", type: "Int"}], # input_schema: %{"type" => "object", "properties" => %{...}, "required" => [...]}, # output: [%{name: "id", type: "String"}, %{name: "status", type: "String"}], # output_schema: %{"type" => "object", "properties" => %{...}, "required" => [...]}, # impl: :__tool_impl_0__ # }] ``` The `input_schema` and `output_schema` fields are proper JSON Schema objects suitable for passing directly to LLM function-calling APIs. `impl` names the compiled entry point for the tool’s `implement` block. ## How Tools Are Registered [Section titled “How Tools Are Registered”](#how-tools-are-registered) Tools are the one cross-module seam in Skein: a `tool.call` in one module resolves to a `tool` declaration in another through the runtime tool registry. When `skein build`, `skein test`, or `skein run` loads a compiled module, every tool it declares is registered automatically — the declared schemas are kept for input validation and the `implement` block becomes the tool’s executable body. Registration is idempotent: reloading a module overwrites its previous entries. From Elixir, the same wiring is available directly: ```elixir Skein.Runtime.Tool.register_module(mod) ``` ## Calling Tools [Section titled “Calling Tools”](#calling-tools) ### From Skein Code [Section titled “From Skein Code”](#from-skein-code) Use the `tool.*` effect calls to interact with tools at runtime. All three return a `Result`, so handle it with `match` or unwrap with `!`, and `tool.call` takes its arguments as a map literal: ```skein module AgentService { capability tool.use(CreateRefund) fn process_refund(customer: String) -> String { match tool.call(CreateRefund, { customer_id: customer, amount: 500 }) { Ok(_) -> "refund created" Err(_) -> "refund failed" } } fn list_available() -> String { match tool.list() { Ok(_) -> "tools listed" Err(_) -> "listing failed" } } fn get_schema() -> Map[String, String] { tool.schema(CreateRefund)! } } ``` | Effect | Description | Returns | | --------------------------- | ------------------------- | ----------------------------------- | | `tool.call(ToolName, args)` | Execute a registered tool | `Result[Map, ToolError]` | | `tool.list()` | List all registered tools | `Result[List[ToolInfo], ToolError]` | | `tool.schema(ToolName)` | Get a tool’s schema | `Result[Map, ToolError]` | ### Capability Required [Section titled “Capability Required”](#capability-required) Tool calls require a `capability tool.use(ToolName)` declaration. Without it, the analyzer produces error E0012. ### Tracing [Section titled “Tracing”](#tracing) Every `tool.call` is automatically traced with: * `kind: :tool` * `method: :call` * `name: "ToolName"` * `duration_us` and `outcome` # Type System > Skein's type system -- built-in types, type checking, schema derivation, and constraint annotations. ## Built-in Types [Section titled “Built-in Types”](#built-in-types) Skein has a set of built-in primitive and parameterized types. Type annotations are required on all function parameters and return types. ### Primitive Types [Section titled “Primitive Types”](#primitive-types) | Type | Skein | Core Erlang / BEAM Representation | | ---------- | --------------------- | --------------------------------- | | `Int` | Integer values | Erlang integer | | `Float` | Floating-point values | Erlang float | | `String` | UTF-8 text | Erlang binary | | `Bool` | `true` / `false` | Erlang atoms `true` / `false` | | `Uuid` | UUID strings | Erlang binary | | `Instant` | Timestamps | Erlang binary | | `Duration` | Time durations | Erlang binary | | `Email` | Email addresses | Erlang binary | | `Url` | URL strings | Erlang binary | ### Parameterized Types [Section titled “Parameterized Types”](#parameterized-types) | Type | Description | | -------------- | -------------------------- | | `Option[T]` | A value that may be absent | | `Result[T, E]` | A value or an error | | `List[T]` | Ordered collection | | `Map[K, V]` | Key-value mapping | | `Set[T]` | Unique collection | Parameterized types use bracket syntax: ```skein Option[String] Result[User, DbError] List[Int] Map[String, User] ``` These parse into `%AST.TypeRef{}` nodes with populated `params` lists. ## Type Annotations [Section titled “Type Annotations”](#type-annotations) Functions require explicit type annotations on all parameters and the return type: ```skein fn add(a: Int, b: Int) -> Int { a + b } fn greet(name: String) -> String { "Hello, ${name}!" } fn is_positive(n: Int) -> Bool { n > 0 } ``` These annotations are stored in the AST as `%AST.TypeRef{}` nodes: ```elixir %AST.TypeRef{name: "Int", params: [], meta: %{line: 1, col: 15, file: "example.skein"}} ``` ## Type Checking [Section titled “Type Checking”](#type-checking) The analyzer’s type checking pass (Pass 2) validates types across function boundaries and expressions. ### What Gets Checked [Section titled “What Gets Checked”](#what-gets-checked) * **Function return types:** The type of the body expression must match the declared return type * **Operator types:** Arithmetic operators require numeric operands, comparison operators produce Bool * **Match arm consistency:** All arms of a match expression must produce the same type * **Function call arity:** The number of arguments must match the function’s parameter count * **Let binding inference:** Types of local bindings are inferred from their initializer expressions ### Type Errors [Section titled “Type Errors”](#type-errors) When a type mismatch is detected, the analyzer produces a structured error: ```json { "code": "E0020", "severity": "error", "message": "Type mismatch: expected String, got Int", "location": {"file": "example.skein", "line": 5, "col": 3}, "fix_hint": "Check that the expression matches the expected type" } ``` ### Error Codes [Section titled “Error Codes”](#error-codes) | Code | Description | | ----- | --------------------------------------------------------------------------------------------------------- | | E0010 | Undefined identifier | | E0011 | Duplicate definition | | E0012 | Missing capability declaration | | E0020 | Type mismatch — including operator type errors and wrong argument counts for fn, stdlib, and effect calls | | E0021 | Non-exhaustive match on a boolean (error) | | E0024 | Unknown type name (error); non-exhaustive match on an enum, Result, or Option (error) | | E0025 | Invalid constraint annotation | ### Match Exhaustiveness [Section titled “Match Exhaustiveness”](#match-exhaustiveness) A match on a closed type (Bool, enums, Result, Option) must cover every case. A missing arm is a **compile error**, not a warning — this code does not compile: ```skein fn check(x: Bool) -> String { match x { true -> "yes" -- error E0021: Non-exhaustive match: missing pattern 'false' } } ``` Add a `false -> ...` arm or a wildcard `_` pattern to make it compile. ## Type Declarations [Section titled “Type Declarations”](#type-declarations) Record types define structured data: ```skein type User { id: Uuid email: String name: String } ``` Enum types define variants with optional associated data: ```skein enum Status { Active Suspended(reason: String) Deleted } ``` ## Constraint Annotations [Section titled “Constraint Annotations”](#constraint-annotations) Type fields support constraint annotations that flow through to generated JSON schemas: ```skein type Money { amount: Int @min(0) currency: String @one_of(["USD", "CAD", "EUR"]) } type Config { max_retries: Int @min(1) @max(10) @default(3) } ``` ### Available Annotations [Section titled “Available Annotations”](#available-annotations) | Annotation | Applies to | JSON Schema | | ----------------- | ----------- | ------------------------- | | `@min(n)` | Int, Float | `"minimum": n` | | `@max(n)` | Int, Float | `"maximum": n` | | `@one_of([...])` | String | `"enum": [...]` | | `@default(v)` | Any | `"default": v` | | `@primary` | Field | — (storage: primary key) | | `@unique` | Field | — (storage: unique index) | | `@description(s)` | Field, Type | `"description": s` | ## JSON Schema Derivation [Section titled “JSON Schema Derivation”](#json-schema-derivation) Type declarations automatically generate JSON schemas via `Skein.CodeGen.SchemaGen`: ```elixir Skein.CodeGen.SchemaGen.to_json_schema(%AST.TypeDecl{ name: "User", fields: [ %AST.Field{name: "id", type: %AST.TypeRef{name: "Uuid", params: []}, annotations: []}, %AST.Field{name: "email", type: %AST.TypeRef{name: "String", params: []}, annotations: []}, %AST.Field{name: "name", type: %AST.TypeRef{name: "String", params: []}, annotations: []} ] }) ``` Produces: ```json { "type": "object", "properties": { "id": {"type": "string", "format": "uuid"}, "email": {"type": "string"}, "name": {"type": "string"} }, "required": ["email", "id", "name"] } ``` ### Type to JSON Schema Mapping [Section titled “Type to JSON Schema Mapping”](#type-to-json-schema-mapping) | Skein Type | JSON Schema | | ----------- | ------------------------------------------------------ | | `String` | `{"type": "string"}` | | `Int` | `{"type": "integer"}` | | `Float` | `{"type": "number"}` | | `Bool` | `{"type": "boolean"}` | | `Uuid` | `{"type": "string", "format": "uuid"}` | | `Email` | `{"type": "string", "format": "email"}` | | `Url` | `{"type": "string", "format": "uri"}` | | `Instant` | `{"type": "string", "format": "date-time"}` | | `Duration` | `{"type": "string"}` | | `Option[T]` | Schema for T (not required) | | `List[T]` | `{"type": "array", "items": }` | | `Set[T]` | `{"type": "array", "uniqueItems": true, "items": }` | | `Map[K, V]` | `{"type": "object", "additionalProperties": }` | ## How Types Are Represented in the AST [Section titled “How Types Are Represented in the AST”](#how-types-are-represented-in-the-ast) ```elixir # Simple type %AST.TypeRef{name: "String", params: []} # Parameterized type %AST.TypeRef{ name: "Result", params: [ %AST.TypeRef{name: "User", params: []}, %AST.TypeRef{name: "DbError", params: []} ] } # Type declaration %AST.TypeDecl{ name: "User", fields: [ %AST.Field{name: "id", type: %AST.TypeRef{name: "Uuid"}, annotations: []}, %AST.Field{name: "email", type: %AST.TypeRef{name: "String"}, annotations: []} ] } ``` ## Enum Variant Matching [Section titled “Enum Variant Matching”](#enum-variant-matching) Enum variants with fields compile to tagged tuples at runtime. You can pattern match on them in `match` expressions: ```skein enum Shape { Circle(radius: Int) Rect(width: Int, height: Int) } fn area(s: Shape) -> Int { match s { Shape.Circle(r) -> r * r Shape.Rect(w, h) -> w * h } } ``` At runtime, `Shape.Circle(5)` is represented as the tuple `{:circle, 5}` and `Shape.Rect(3, 4)` as `{:rect, 3, 4}`. Simple variants without fields (like `Active`) compile to atoms (`:active`). The analyzer checks that all enum variants are covered in a match expression (a missing variant is error E0024). A wildcard `_` arm satisfies this check. ### Exhaustiveness Caveat [Section titled “Exhaustiveness Caveat”](#exhaustiveness-caveat) Exhaustiveness checking operates at the **variant level**, not at the value level within variant fields. For example: ```skein enum Action { GetUser(id: Int) DeleteUser(id: Int) } fn handle(a: Action) -> String { match a { Action.GetUser(5) -> "found user 5" -- only matches id=5 Action.DeleteUser(id) -> "deleted" } } ``` Variant-level coverage is satisfied because an arm exists for `GetUser` — but at runtime, `Action.GetUser(10)` would raise `case_clause` because the literal `5` does not match `10`. The analyzer warns about exactly this (W0004): “Match on Action covers only specific values of GetUser: other GetUser(…) values raise case\_clause at runtime”. To clear the warning, use a variable pattern or add a wildcard arm: ```skein fn handle(a: Action) -> String { match a { Action.GetUser(id) -> "found user" -- matches all GetUser values Action.DeleteUser(id) -> "deleted" } } ``` Variant-level coverage is an error-grade check (a missing variant is E0024); value-level coverage inside a variant is warning-grade (W0004) — the match still compiles, but the warning tells you a runtime `case_clause` is possible. # Agent Primer > Compact Skein language primer scaffolded into every project as AGENTS.md — syntax cheatsheet, capability rules, agent basics, gotchas, and CLI commands for coding agents. This page is the source of the generated section of `AGENTS.md` that `skein new` and `skein agents` write into Skein projects. It is a compact primer for coding agents working inside a Skein project, not a guide to the Skein compiler codebase itself. ## What Skein Is [Section titled “What Skein Is”](#what-skein-is) Skein is a programming language for cloud services where AI agents are first-class constructs. It compiles to BEAM bytecode and runs on the Erlang VM. Source files use the `.skein` extension. Comments start with `--`. ## Project Layout and Commands [Section titled “Project Layout and Commands”](#project-layout-and-commands) ```plaintext skein.toml -- project config src/*.skein -- source modules (one module or agent per file) test/*.skein -- test modules ``` | Command | Purpose | | ---------------------- | ----------------------------------------------------------------------------------- | | `skein build` | Compile all `.skein` files under `src/` | | `skein test` | Run all `test`/`scenario` declarations in `src/` and `test/` | | `skein run` | Start the service (HTTP handlers; `--port `, default 4000) | | `skein compile ` | Compile a single file | | `skein trace` | View recent trace spans (`--last `, `--kind `) | | `skein agents` | Regenerate the generated block of this file | | `skein mcp` | Start the Skein MCP server (stdio) for spec lookup, docs search, and compile checks | Compiler errors are structured: every error has a `code`, `message`, `location`, and usually a `fix_hint` and `fix_code` telling you exactly what to change. Read them — they are written for agents. ## Syntax Cheatsheet [Section titled “Syntax Cheatsheet”](#syntax-cheatsheet) ```plaintext -- A module: capabilities, types, functions, handlers, tools, tests module UserService { capability http.in capability store.table("users", User) type User { id: Uuid @primary email: Email @unique name: String } fn greet(name: String) -> String { "Hello, ${name}!" -- interpolation: ${ident} / ${record.field} } handler http GET "/users/:id" (req) -> { let id = Uuid.parse(req.params.id)! match store.users.get(id) { Ok(u) -> respond.json(200, u) Err(_) -> respond.json(404, { error: "not found" }) } } test "greet works" { assert greet("World") == "Hello, World!" } } ``` Core forms: * `let x = expr` — single-assignment binding; type is inferred. * `match expr { Pattern -> expr ... }` — arms must cover all enum variants and all return the same type. * `expr |> fn_name(args)` — pipe. * `expr!` — unwrap a `Result`, crashing on `Err`. `expr?` — propagate the `Err` to the caller (enclosing function must return `Result`). * Function params and return types are always annotated: `fn add(a: Int, b: Int) -> Int { a + b }`. * Calls may pass arguments by name after any positional ones: `add(b: 2, a: 1)`, `llm.chat(model: "...", system: "...", input: t)`. Named args work for same-module fns and documented effect signatures; stdlib calls are positional only. Never put a positional arg after a named one. * Built-in types: `Int`, `Float`, `String`, `Bool`, `Uuid`, `Instant`, `Duration`, `Email`, `Url`, `Option[T]`, `Result[T, E]`, `List[T]`, `Map[K, V]`, `Set[T]`. Field constraints: `@min(n)`, `@max(n)`, `@one_of([...])`, `@default(v)`, `@description(s)`, `@primary`, `@unique`. * Stdlib modules (`String`, `Int`, `Float`, `List`, `Map`, `Set`, `Option`, `Result`, `Uuid`, `Instant`, `Duration`) are ambient — no capability or declaration needed: `String.upcase(s)`, `List.map(items, &double)`. ## Capabilities and Effects [Section titled “Capabilities and Effects”](#capabilities-and-effects) Every effect call requires a matching `capability` declaration in the same module or agent. There are no exceptions. | Capability | Grants | | ------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `http.in` | Declaring `handler http ...` | | `http.out("host")` | `http.get/post/put/delete` to that host | | `store.table("name", RecordType)` | `store..get/put/delete/query`, typed against `RecordType` (a declared `type` with exactly one `@primary` field) | | `memory.kv("namespace")` | `memory.put/get/delete/list` | | `model("provider", "model")` | `llm.chat/json/stream/embed` | | `tool.use(Mod.ToolName)` | `tool.call(Mod.ToolName, { ... })` | | `queue.publish("name")` / `queue.consume("name")` | `queue.publish` / `handler queue` | | `topic.publish("name")` / `topic.consume("name")` | `topic.publish` / `handler topic` | | `schedule.trigger("name")` | `handler schedule` | | `event.log("stream")` | `event.log(name, data)` | | `process.spawn` / `timer` | `process.spawn` / `timer.after/interval/cancel` | | `uuid` / `instant` | `uuid.new()` / `instant.now()` — all nondeterminism is capability-gated (deterministic under `skein test`) | `emit EventName { field: value }` and `trace.annotate(key, value)` need no capability. `idempotent(key)` (handlers only) skips re-processing a key. ## Modules Are Sealed: Tools Are the Only Cross-Module Seam [Section titled “Modules Are Sealed: Tools Are the Only Cross-Module Seam”](#modules-are-sealed-tools-are-the-only-cross-module-seam) Functions, types, and handlers are private to their module. There is no `import` and no `OtherModule.fn_name(...)` — that is a compile error (E0016). To share behavior across modules, declare a `tool` in one module and call it from another: ```plaintext -- callee module tool Billing.CreateRefund { description: "Issue a refund" input { customer_id: String, amount: Int @min(1) } output { id: String, status: String } implement { ... } } -- caller module (or agent) capability tool.use(Billing.CreateRefund) let result = tool.call(Billing.CreateRefund, { customer_id: cid, amount: 500 })! -- ! unwraps the Result ``` ## Agents, Phases, and Transitions [Section titled “Agents, Phases, and Transitions”](#agents-phases-and-transitions) Agents declare state, a `Phase` enum with legal transitions, and `on` handlers. They can be top-level (one per file) or nested inside a module — a nested agent compiles to its own BEAM module and sees the enclosing module’s types, capabilities, and functions in addition to its own (a phase handler may call the module’s `fn`s directly). Transitions are checked at compile time — `transition()` to a phase not listed in the current phase’s `-> [...]` list is an error (E0030), and every phase needs an `on phase` handler (E0032). ```plaintext agent RefundAgent { capability memory.kv("refund_sessions") state { ticket_id: String } enum Phase { Analyze -> [Refund, Done] Refund -> [Done] Done -> [] } on start(ticket_id: String) -> { -- typed params, NOT `on start(ctx)` memory.put("ticket_id", ticket_id) transition(Phase.Analyze) } on phase(Phase.Analyze) -> { -- decide, then: transition(Phase.Refund) } on phase(Phase.Refund) -> { transition(Phase.Done) } on phase(Phase.Done) -> { stop() } -- stop() needs the parens } ``` Agent lifecycle calls (inside agents only): `transition(Phase.X)`, `stop()`, `suspend(reason)`. There is no in-agent `resume` — a suspended agent is resumed from outside by the host-side `Skein.Runtime.Agent.resume(pid, phase)`. Memory keys are automatically scoped per agent instance. ## Known Gotchas [Section titled “Known Gotchas”](#known-gotchas) * **Effects return a `Result` — unwrap it.** `llm.chat`, `llm.json[T]`, `req.json[T]`, `store..get/put/delete/query`, `http.get/post/...`, `queue.publish`, `topic.publish`, and `tool.call` all return `Result[T, E]` at runtime. Use `expr!` (crash on `Err`) or `expr?` (propagate) before touching the value — otherwise you get an opaque BEAM crash. This is the single most common trap. * **No `if`/`else`.** Conditionals are `match` on a `Bool`: `match cond { true -> ... false -> ... }`. * **`store..get` not-found is a named variant**: match it as `Err(StoreError.NotFound) -> ...` (or the bare `Err(NotFound)`); `Err(_)` still works as a catch-all. * Tool `implement` bodies see their input fields directly in scope (`amount`, not `input.amount`) and must return the output shape wrapped in `Ok({ ... })`; `input`/`output` are contextual keywords only inside tool declarations. * `stop()` must be called with parentheses. * `on start` takes typed parameters (`on start(order_id: String)`), not a bare context argument. * Match arms must all return the same type, and enum matches must cover every variant (or use `_`). * One top-level module (or agent) per file; agents may be nested inside a module’s body. * `handler queue`/`handler topic` take the queue/topic name as a string route; `handler schedule` handlers take no payload parameter. * String interpolation takes identifiers and field access only — `${name}`, `${user.name}`; bind any other expression with `let` first. Comments are `--` (not `//` or `#`). ## Going Deeper [Section titled “Going Deeper”](#going-deeper) * Full language spec, examples, and stdlib reference (agent-friendly): * (entry point) * (compact) * (full corpus) * On-demand spec lookup, docs search, and structured compile checks without leaving your editor: register the Skein MCP server. It speaks MCP over stdio: ```bash # Claude Code claude mcp add skein -- skein mcp ``` Tools: `skein_spec_lookup` (fetch a spec section), `skein_docs_search` (search the spec corpus), `skein_compile_check` (compile a file or project and get structured JSON errors with fix hints). # Agent Quick Reference > Complete reference for AI agents working with the Skein language project — conventions, structure, commands, and rules in one page. ## Project Identity [Section titled “Project Identity”](#project-identity) Skein is a programming language that compiles to BEAM bytecode and runs on the Erlang VM (OTP). The compiler and toolchain are implemented in Elixir. Skein is designed for building cloud services where AI agents are first-class constructs. ## Repository Structure [Section titled “Repository Structure”](#repository-structure) ```plaintext skein/ ├── mix.exs # Root umbrella project ├── CLAUDE.md # Agent instructions ├── .docs-config.json # Documentation site config ├── docs/ │ ├── SKEIN_SPEC.md # Complete language specification │ ├── ARCHITECTURE.md # Compiler and runtime architecture │ ├── ROADMAP.md # Development roadmap │ └── site/ # Astro + Starlight documentation site ├── editors/ │ └── vscode/ # VS Code extension (grammar, snippets, LSP client) ├── apps/ │ ├── skein_compiler/ # Lexer, parser, analyzer, code generator │ ├── skein_runtime/ # OTP behaviours and runtime support │ ├── skein_cli/ # CLI tooling │ └── skein_lsp/ # Language Server Protocol implementation └── examples/ # Canonical .skein programs ``` ## Technology Stack [Section titled “Technology Stack”](#technology-stack) | Component | Choice | | ----------------- | ----------------------------------- | | Compiler language | Elixir | | Lexer | Hand-written binary matching | | Parser | Hand-written recursive descent | | IR target | Core Erlang | | Agent runtime | OTP’s `:gen_statem` (used directly) | | HTTP server | Bandit + Plug | | Storage | Ecto + SQLite3 | | Language server | GenLSP | | Testing | ExUnit + StreamData + PropCheck | ## Commands [Section titled “Commands”](#commands) | Command | Purpose | | -------------- | -------------------- | | `mix deps.get` | Install dependencies | | `mix compile` | Compile the compiler | | `mix test` | Run all tests | | `mix format` | Format code | ## Compilation Pipeline [Section titled “Compilation Pipeline”](#compilation-pipeline) Source (.skein) → Lexer → Parser → Analyzer → CodeGen → BEAM bytecode 1. **Lexer** (`Skein.Lexer`) — source text to token stream 2. **Parser** (`Skein.Parser`) — token stream to AST 3. **Analyzer** (`Skein.Analyzer`) — AST to annotated AST (multi-pass: name resolution, type checking, capability checking, transition checking, plus lint passes for unused bindings/capabilities and unreachable code) 4. **CodeGen** (`Skein.CodeGen.CoreErlang`) — AST to Core Erlang to BEAM bytecode ## Key Modules [Section titled “Key Modules”](#key-modules) ### Compiler [Section titled “Compiler”](#compiler) | Module | Location | Purpose | | -------------------------- | ------------------------------------------------------ | -------------------------- | | `Skein.Lexer` | `apps/skein_compiler/lib/skein/lexer.ex` | Tokenizer | | `Skein.Parser` | `apps/skein_compiler/lib/skein/parser.ex` | AST construction | | `Skein.AST` | `apps/skein_compiler/lib/skein/ast.ex` | AST node definitions | | `Skein.Analyzer` | `apps/skein_compiler/lib/skein/analyzer.ex` | Type + capability checking | | `Skein.CodeGen.CoreErlang` | `apps/skein_compiler/lib/skein/codegen/core_erlang.ex` | Code generation | | `Skein.CodeGen.SchemaGen` | `apps/skein_compiler/lib/skein/codegen/schema_gen.ex` | JSON Schema derivation | | `Skein.Error` | `apps/skein_compiler/lib/skein/error.ex` | Structured error types | | `Skein.Compiler` | `apps/skein_compiler/lib/skein_compiler.ex` | Pipeline entry point | ### Runtime [Section titled “Runtime”](#runtime) | Module | Location | Purpose | | ---------------------------- | ------------------------------------------------------- | --------------------------------------- | | `Skein.Runtime.Agent` | `apps/skein_runtime/lib/skein/runtime/agent.ex` | `:gen_statem`-based agent lifecycle | | `Skein.Runtime.Http` | `apps/skein_runtime/lib/skein/runtime/http.ex` | HTTP client with capability enforcement | | `Skein.Runtime.Capability` | `apps/skein_runtime/lib/skein/runtime/capability.ex` | Runtime capability validation | | `Skein.Runtime.Handler` | `apps/skein_runtime/lib/skein/runtime/handler.ex` | HTTP request dispatch | | `Skein.Runtime.Store` | `apps/skein_runtime/lib/skein/runtime/store.ex` | ETS-backed storage (default) | | `Skein.Runtime.StoreEcto` | `apps/skein_runtime/lib/skein/runtime/store_ecto.ex` | Ecto/SQLite-backed storage | | `Skein.Runtime.EctoSchema` | `apps/skein_runtime/lib/skein/runtime/ecto_schema.ex` | Dynamic Ecto schema generation | | `Skein.Runtime.MigrationGen` | `apps/skein_runtime/lib/skein/runtime/migration_gen.ex` | Ecto migration generation | | `Skein.Runtime.Repo` | `apps/skein_runtime/lib/skein/runtime/repo.ex` | Ecto Repo (SQLite3) | | `Skein.Runtime.Memory` | `apps/skein_runtime/lib/skein/runtime/memory.ex` | Scoped KV memory | | `Skein.Runtime.Llm` | `apps/skein_runtime/lib/skein/runtime/llm.ex` | LLM client with schema-constrained JSON | | `Skein.Runtime.Server` | `apps/skein_runtime/lib/skein/runtime/server.ex` | HTTP server | | `Skein.Runtime.Trace` | `apps/skein_runtime/lib/skein/runtime/trace.ex` | Trace span recording | ### Language Server [Section titled “Language Server”](#language-server) | Module | Location | Purpose | | -------------------------- | ------------------------------------------------- | ------------------------------- | | `Skein.Lsp.Server` | `apps/skein_lsp/lib/skein/lsp/server.ex` | Main LSP server | | `Skein.Lsp.Diagnostics` | `apps/skein_lsp/lib/skein/lsp/diagnostics.ex` | Compiler error → LSP diagnostic | | `Skein.Lsp.Symbols` | `apps/skein_lsp/lib/skein/lsp/symbols.ex` | Document symbol extraction | | `Skein.Lsp.HoverProvider` | `apps/skein_lsp/lib/skein/lsp/hover_provider.ex` | Hover info and go-to-definition | | `Skein.Lsp.Completions` | `apps/skein_lsp/lib/skein/lsp/completions.ex` | Context-aware code completion | | `Skein.Lsp.SemanticTokens` | `apps/skein_lsp/lib/skein/lsp/semantic_tokens.ex` | Semantic token encoding | ## Coding Conventions [Section titled “Coding Conventions”](#coding-conventions) * Follow `mix format` style * Typespecs (`@spec`) on all public functions * `@moduledoc` and `@doc` on all public modules and functions * Pattern match in function heads over case/cond * Pipe operator for data transformations * `{:ok, result}` / `{:error, errors}` tuples throughout the pipeline * TDD is mandatory — tests before or alongside implementation ## Error Format [Section titled “Error Format”](#error-format) Every compiler error is a `%Skein.Error{}` struct with fields: `code`, `severity`, `message`, `location`, `context`, `fix_hint`, `fix_code`. Errors serialize to JSON. ## Terminology [Section titled “Terminology”](#terminology) | Canonical Term | Do Not Use | | -------------- | --------------------- | | agent | bot, assistant | | module | class, package | | handler | endpoint, route | | phase | state, stage | | capability | permission, privilege | ## Current Status [Section titled “Current Status”](#current-status) All phases complete — MVP reached. The full compilation pipeline supports: * Modules with functions, let bindings, match expressions, pipes, string interpolation * Type checking with inference, JSON schema derivation, constraint annotations * Capability-based security with compile-time and runtime enforcement * HTTP/queue/schedule handlers with route matching and path parameters * ETS-backed store operations with capability gating and typed tables (C5, #255) — `capability store.table("users", User)` names the record type, operations are type-checked, writes are schema-checked at runtime (the Ecto/SQLite path remains unwired library code) * Agent state machines with phase transitions, compile-time transition validation * Scoped KV memory with namespace isolation * LLM client with pluggable backends, schema-validated JSON decoding for `llm.json[T]` (responses are recursively validated against the derived schema — C3, #298; violations are `Err(LlmError.InvalidSchema(violations))`), and streaming * Automatic trace recording for all effect calls * Full CLI tooling (new, build, test, run, trace) * VS Code extension with LSP (diagnostics, symbols, hover, completions, go-to-definition) **Test suite:** the full umbrella unit and property suites run green — see CI for current counts # JSON Output (--json) > The stable, versioned JSON envelope emitted by skein compile, build, test, and trace under --json — the machine-readable agent contract for Skein tooling. **Status:** stable from v1.0.0 onward (governed by [Stability & Versioning](/Skein/reference/stability/)). `skein compile`, `skein build`, `skein test`, and `skein trace` accept a `--json` flag that replaces their human-readable output with a single machine-readable JSON document on stdout. This is the contract coding agents and CI scripts should parse — the plain text format is for humans and is not a stable interface. The flag is presentation-only: it never changes what a command *does*, only how the result is reported. It may appear anywhere in the argument list (`skein test --json`, `skein test --json ./project`, `skein test ./project --json` are equivalent). ## The envelope [Section titled “The envelope”](#the-envelope) Every `--json` command prints exactly one JSON object, terminated by a single newline, with the same three top-level keys: ```json { "schema": "skein./v1", "ok": true, "data": { } } ``` | Key | Type | Meaning | | -------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `schema` | string | Identifies the payload shape and its version: `skein.compile/v1`, `skein.build/v1`, `skein.test/v1`, `skein.trace/v1`. The `/vN` suffix is bumped only on a breaking change to `data`; additive fields do not bump it. | | `ok` | boolean | The machine success signal. `true` means the command succeeded with nothing to fix. The process **exit code mirrors this**: `0` when `ok` is `true`, `1` otherwise — so a script can branch on either. | | `data` | object | Command-specific payload, documented below. | > Key **order** is not significant (this is JSON) — parse by key, never by position. New fields may be added to any `data` object within the same schema version, so ignore unknown keys. ### Top-level errors [Section titled “Top-level errors”](#top-level-errors) A failure that prevents the command from producing its normal result — a bad flag, a missing file, or an empty project — is reported as `ok: false` with a single human-readable string: ```json { "schema": "skein.test/v1", "ok": false, "data": { "message": "No .skein files found in ./src/" } } ``` This is distinct from per-file or per-test failures (which appear inside the normal `data` shape, described below). Detect a top-level error by the presence of `data.message`. ## skein compile (the “check” command) [Section titled “skein compile (the “check” command)”](#skein-compile-the-check-command) Compiles and checks a single `.skein` file. `ok` is `true` when there are no errors (warnings do not affect `ok`). ```json { "schema": "skein.compile/v1", "ok": true, "data": { "module": "Skein.User.Demo", "errors": [], "warnings": [] } } ``` | `data` field | Type | Notes | | ------------ | ------------------------------------------ | -------------------------------------------------------------- | | `module` | string \| null | The compiled module’s name, or `null` when compilation failed. | | `errors` | array of [diagnostic](#diagnostic-objects) | Empty when `ok` is `true`. | | `warnings` | array of [diagnostic](#diagnostic-objects) | Present whether or not compilation succeeded. | ## skein build [Section titled “skein build”](#skein-build) Compiles every `.skein` file under a project’s `src/` tree. `ok` is `true` when no file failed (`errors == 0`). ```json { "schema": "skein.build/v1", "ok": true, "data": { "compiled": 2, "errors": 0, "modules": ["Skein.User.A", "Skein.User.B"], "failed": [] } } ``` | `data` field | Type | Notes | | ------------ | --------------------------- | ------------------------------------------------------------------------------------ | | `compiled` | integer | Count of modules that compiled. | | `errors` | integer | Count of files that failed to compile. | | `modules` | array of string | Names of compiled modules. | | `failed` | array of `{ file, errors }` | Each entry has the source `file` and an array of [diagnostics](#diagnostic-objects). | ## skein test [Section titled “skein test”](#skein-test) Runs every test/scenario/golden across a project (compiling `src/` then `test/` first). `ok` is `true` only when **no test failed and every file compiled** (`failed == 0 and compile_errors == 0`). ```json { "schema": "skein.test/v1", "ok": false, "data": { "total": 2, "passed": 1, "failed": 1, "files": 1, "compile_errors": 0, "compile_failed": [], "results": [ { "description": "greets through the tool", "status": "passed", "kind": "scenario", "file": "test/main_test.skein" }, { "description": "refund sends id header", "status": "failed", "kind": "scenario", "file": "test/refund_test.skein", "error": "expected status \"ok\", got \"error\"", "location": "test/refund_test.skein:14" } ] } } ``` | `data` field | Type | Notes | | ----------------------------- | -------------------------------------------- | ----------------------------------------------------------- | | `total` / `passed` / `failed` | integer | Counts over the tests that ran. | | `files` | integer | Number of files that contributed tests. | | `compile_errors` | integer | Files that failed to compile and were therefore not tested. | | `compile_failed` | array of `{ file, errors }` | Same shape as `build`’s `failed`. | | `results` | array of [test result](#test-result-objects) | One per test/scenario/golden that ran. | ### Test result objects [Section titled “Test result objects”](#test-result-objects) | Field | Type | Notes | | ------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `description` | string | The test’s declared description. | | `status` | string | `"passed"` or `"failed"`. | | `kind` | string | `"test"`, `"scenario"`, or `"golden"`. | | `file` | string | Source file the test came from. | | `error` | string | **Failures only.** The failure message — including the structured detail Skein’s runner produces for a blocked live effect, an exhausted/mismatched replay trace, or an assertion mismatch. | | `location` | string | **Failures with a known site only.** `file:line` of the failing assertion. | ## skein trace [Section titled “skein trace”](#skein-trace) Reports recent trace spans from the runtime trace store. `ok` is always `true` on a successful read (an empty trace is success with `count: 0`). ```json { "schema": "skein.trace/v1", "ok": true, "data": { "count": 2, "spans": [ { "kind": "http", "method": "get", "url": "/refunds", "status": 200, "outcome": "ok", "duration_us": 1500 }, { "kind": "llm", "method": "chat", "url": "anthropic", "outcome": "ok", "duration_us": 3200 } ] } } ``` Each span is projected down to a **fixed, stable field set** — `kind`, `method`, `url`, `status`, `outcome`, `duration_us` — so arbitrary recorded payloads never leak into (or break) the contract. Fields a given span does not carry are omitted, so every span kind renders without a fixed key set; only `kind` is effectively always present. | Field | Type | Notes | | ------------- | ------- | ------------------------------------------------------------------------- | | `kind` | string | Span kind: `http`, `llm`, `tool`, `memory`, `store`, `uuid`, `instant`, … | | `method` | string | e.g. `get`/`post` for http, `chat`/`json` for llm. Omitted when absent. | | `url` | string | Request URL or backend name. Omitted when absent. | | `status` | integer | HTTP status code. Omitted when absent. | | `outcome` | string | `ok` / `err`. Omitted when absent. | | `duration_us` | integer | Span duration in **microseconds**. Omitted when absent. | ## Diagnostic objects [Section titled “Diagnostic objects”](#diagnostic-objects) Compiler errors and warnings (in `compile`/`build`) serialize as Skein’s structured [`Skein.Error`](/Skein/compiler/errors/) — the same JSON the [MCP server](/Skein/editor/mcp-server/) returns. Every diagnostic carries: | Field | Type | Notes | | ----------- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------- | | `code` | string | Stable diagnostic code, e.g. `E0020`, `W0002`. | | `severity` | string | `"error"` or `"warning"`. | | `message` | string | Human-readable description. | | `location` | `{ file, line, col }` | Where the diagnostic applies. | | `span` | `{ start, end }` \| null | End-exclusive source span when known. | | `context` | string \| null | The offending source line, when available. | | `fix_hint` | string \| null | A human-readable suggestion. | | `fix_code` | string \| null | Suggested replacement text. | | `edit_kind` | string \| null | Machine-applicability discriminator (`replace`, `insert_before`, …); `null` means `fix_code` is a template, not a literal edit. | A top-level error reason that is not a structured diagnostic (a usage or filesystem message) is reported as a minimal `{ "message": "..." }` object in the same `errors` array, so `data.errors` is uniformly a list of objects. ## TUI / interactivity [Section titled “TUI / interactivity”](#tui--interactivity) `--json` is mutually independent from the interactive seam. `trace` also accepts `--interactive` (opt into a TTY front-end where one exists) and `--no-tui` / `SKEIN_NO_TUI` (force plain). None of these change `--json` output: a JSON request always produces the byte-for-byte envelope above, never a TUI. MCP, LSP, and any non-TTY stdout likewise never route through a TUI. # Stability & Versioning > What semver means for Skein at 1.0 — the stability class of every public surface, release cadence, spec versioning, and the deprecation policy. **Status: FROZEN at the v1.0.0-rc.5 gate** (Wave F, 2026-07-02, #332). Every Stable surface below is declared frozen and guarded by an executable gate: the frozen vectors under `conformance/freeze/` (keywords, the diagnostics registry, the effect ABI and error shapes, JSON Schema derivation, the CLI and `skein.toml` surface, compiled-metadata classes, and EventStore persisted event shapes) fail the test suite on any drift, and release-readiness asserts them before a tag. Between rc.5 and v1.0.0 GA a frozen surface changes only for a release-blocking defect, with the vector diff called out in the CHANGELOG; the semver *promises* below bind when v1.0.0 tags. v1.0.0 has **not** been released yet — the candidate soaks first; see the [roadmap](/roadmap/overview/) for the release posture. Skein is a language, a runtime, and a toolchain that ship together as one versioned artifact. “Semver” therefore means more here than it does for a library: a Skein version number makes promises about source programs, about machine-consumed compiler output, and about persisted runtime data — not just about an Elixir API. A version is `MAJOR.MINOR.PATCH`: * **PATCH** (`1.0.x`) — bug fixes only. No surface changes of any kind: a program that compiled cleanly keeps compiling with identical diagnostics, and no new language surface, flags, or config keys appear. * **MINOR** (`1.x.0`) — additive changes. New language constructs, new stdlib functions, new error codes, new CLI flags and `skein.toml` keys, new effect methods. Existing programs compile and behave identically. * **MAJOR** (`2.0.0`) — breaking changes, including removals of deprecated surface. ## Stability classes [Section titled “Stability classes”](#stability-classes) Every public surface belongs to one of three classes: * **Stable** — covered by the promises above. Breaking it requires a major release. * **Evolving** — may gain capability in minor releases; consumers must tolerate additions (new map keys, new fields, new enum members). Removals and shape changes still require a major. * **Internal** — no compatibility promise at any version. Do not build on it. | Surface | Class | Notes | | ---------------------------------------------------------------------------------------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Language: spec grammar + semantics | **Stable** | See “The language” below | | Error and warning codes (`E####`/`W####`) | **Stable, append-only** | See “Error codes” below | | Structured-error ABI (effect error enum variants + their lowered tuple forms, spec §6) | **Stable** | The matchable forms are pinned by `Skein.EffectABI.error_enums/0` and the runtime ABI-matrix tests (C2/#297). New variants in minors; existing variants never change shape. The blocked-live `LiveEffectError` raise stays uncatchable — a frozen decision | | Compiled-module metadata (`__handlers__/0`, `__tools__/0`, `__tests__/0`, `__supervisors__/0`) | **Evolving** | Entries may gain fields in minors; existing fields never change meaning | | EventStore persisted event shapes (SQLite) | **Stable** | Frozen at the Wave F gate (#332): the persisted-and-reloaded shapes are pinned by frozen vectors (the inline persisted-shape vectors in `event_store_freeze_test.exs` + the persistence round-trip tests). The SQLite backend rides the ordinary append path (`Persistence.enable/1`; `skein run` enables it by default, #299). Shapes gain fields in minors, never change or lose them before a major. Replay depends on them; see “Stored traces” below | | `skein.toml` format | **Stable** | New keys in minors; unknown keys are never errors | | CLI commands and flags | **Stable** | New commands/flags in minors; removals/renames major | | JSON Schema derivation | **Stable** | The schema derived from a given type declaration only changes in a major | | Tooling protocols: MCP tool result shapes, LSP diagnostic `data` payload | **Evolving** | Additive in minors — consumers must ignore unknown keys | | Compiler module APIs (`Skein.Lexer`, `Skein.Parser`, `Skein.Analyzer`, `Skein.CodeGen.*`) | **Internal** | The CLI/MCP/LSP are the supported entry points | | Core Erlang output shape | **Internal** | Only the *behavior* of compiled code is promised | | Runtime module APIs not reachable from Skein source | **Internal** | e.g. `Skein.Runtime.EtsTables`, backend internals | ### The language [Section titled “The language”](#the-language) The spec (`docs/SKEIN_SPEC.md`) is the contract. A breaking change is anything that makes a previously valid program fail to compile or change behavior — including new *reserved* keywords, which would break programs using that word as an identifier. Skein’s contextual-keyword machinery exists precisely to keep additions non-breaking: new constructs prefer contextual keywords (recognized only in their position, usable as identifiers everywhere else — as `if` is in match-arm guards) over reserved ones. New *warnings* are additive and may appear in minors; new *errors* on previously-accepted programs are breaking, with one exception: a program that only compiled because of a compiler bug (accepted surface the spec already prohibited) may start erroring in a patch, with a changelog note. ### Error codes [Section titled “Error codes”](#error-codes) Diagnostic codes are machine-consumed — LSP code actions, MCP `skein_compile_check`, and agents key off the `code` field. After 1.0: * Codes are **append-only**: a code is never renumbered, repurposed, or deleted (a code can stop being emitted, but its meaning is retired with it). * The structured shape (`code`, `severity`, `message`, `location`, `fix_hint`, `fix_code`, `context`, `span`, `edit_kind`) only gains fields. * `edit_kind` values are append-only: an existing kind never changes meaning (spec §7 defines the application semantics of each). * Message *text* may be reworded in minors; tooling must key off `code`, not message text. ### Stored traces (EventStore schema) [Section titled “Stored traces (EventStore schema)”](#stored-traces-eventstore-schema) Recorded traces are replayable data: events recorded by any 1.x runtime replay on every later 1.y (y ≥ x). Event shapes only gain fields within 1.x; existing fields are never renamed or repurposed. A major release may change the storage schema, in which case it must document a JSON export path for traces worth keeping (no export command ships in 1.0). ### `skein.toml` and the CLI [Section titled “skein.toml and the CLI”](#skeintoml-and-the-cli) Config keys and CLI flags are additive within a major. Unknown `skein.toml` keys are ignored (never errors), so a project file written for 1.x parses on 1.0. Flag *defaults* don’t change within a major. ### JSON Schema derivation [Section titled “JSON Schema derivation”](#json-schema-derivation) External systems consume derived schemas (LLM structured output, tool input validation). For a fixed type declaration, the derived schema is stable within a major: same properties, same `required`, same variant encoding. New type-system constructs map to *new* schema output without altering the output for existing declarations. ## Release cadence and branching [Section titled “Release cadence and branching”](#release-cadence-and-branching) * Releases are cut from `main` by the auto-tag flow: a green version-bump merge tags and publishes (binaries, docs snapshot, GitHub Release). * There are no long-lived release branches by default. A `release/1.x` branch is created on demand only when a severe defect needs a patch after `main` has moved on to unreleasable work. * Patches ship as needed; minors ship when additive work accumulates; there is no calendar cadence. ## Spec versioning [Section titled “Spec versioning”](#spec-versioning) The spec is frozen per minor: each `1.x.0` release snapshots the docs (including the spec and the `llms*.txt` endpoints) under that version, and the spec does not change for the lifetime of that minor except for erratum fixes that don’t alter surface. The spec version is the binary version that shipped it. ## Deprecation policy [Section titled “Deprecation policy”](#deprecation-policy) * Surface is deprecated in a minor (compiler warning or documented notice), stays functional for the remainder of the major, and is removed no earlier than the next major. * Diagnostic *hints* that mention old names (like the `queue.in` → `queue.consume` rename hint on E0012) are not deprecated surface; they stay as long as they help. * Anything still deprecated when a major ships is removed in that major — majors do not carry deprecated surface forward. # Standard Library > Reference for Skein's built-in standard library modules. Skein ships with 11 standard library modules available in every Skein program without imports. Stdlib calls compile to static function calls on the corresponding `Skein.Runtime.Stdlib.*` Elixir module. Detailed API docs Full API documentation with typespecs is available in the [ExDoc API Reference](/Skein/api/). Generate locally with `mix docs` from the project root. ## String [Section titled “String”](#string) Text operations. All functions operate on UTF-8 binaries. ```skein let name = " Hello, World! " String.trim(name) -- "Hello, World!" String.upcase("hello") -- "HELLO" String.length("hi") -- 2 String.contains("abc", "b") -- true String.split("a,b,c", ",") -- ["a", "b", "c"] String.replace("hi", "h", "H") -- "Hi" ``` | Function | Signature | Description | | ------------- | ------------------------------------ | ---------------------------------- | | `length` | `(String) -> Int` | Number of Unicode graphemes | | `slice` | `(String, Int, Int) -> String` | Substring from offset with length | | `contains` | `(String, String) -> Bool` | Whether string contains substring | | `split` | `(String, String) -> List[String]` | Split on delimiter | | `trim` | `(String) -> String` | Remove leading/trailing whitespace | | `upcase` | `(String) -> String` | Convert to uppercase | | `downcase` | `(String) -> String` | Convert to lowercase | | `starts_with` | `(String, String) -> Bool` | Check prefix | | `ends_with` | `(String, String) -> Bool` | Check suffix | | `replace` | `(String, String, String) -> String` | Replace all occurrences | ## Int [Section titled “Int”](#int) Integer operations. ```skein Int.parse("42") -- Ok(42) Int.abs(-5) -- 5 Int.clamp(15, 0, 10) -- 10 Int.min(3, 7) -- 3 ``` | Function | Signature | Description | | ----------- | --------------------------------- | ------------------------------ | | `parse` | `(String) -> Result[Int, String]` | Parse string to integer | | `to_string` | `(Int) -> String` | Convert to string | | `abs` | `(Int) -> Int` | Absolute value | | `min` | `(Int, Int) -> Int` | Smaller of two values | | `max` | `(Int, Int) -> Int` | Larger of two values | | `clamp` | `(Int, Int, Int) -> Int` | Constrain to range \[min, max] | ## Float [Section titled “Float”](#float) Floating-point operations. ```skein Float.parse("3.14") -- Ok(3.14) Float.round(3.456, 2) -- 3.46 Float.ceil(2.1) -- 3 Float.floor(2.9) -- 2 ``` | Function | Signature | Description | | ----------- | ----------------------------------- | --------------------------------- | | `parse` | `(String) -> Result[Float, String]` | Parse string to float | | `to_string` | `(Float) -> String` | Convert to string | | `round` | `(Float, Int) -> Float` | Round to N decimal places | | `ceil` | `(Float) -> Int` | Round up to the nearest integer | | `floor` | `(Float) -> Int` | Round down to the nearest integer | ## List [Section titled “List”](#list) Collection operations. Lists are ordered, homogeneous sequences: a `List[T]` holds elements that all share one type `T`, so a mixed literal like `[1, "two"]` is a compile error. ```skein fn double(x: Int) -> Int { x * 2 } fn over_three(x: Int) -> Bool { x > 3 } fn add(acc: Int, x: Int) -> Int { acc + x } let nums = [1, 2, 3, 4, 5] List.map(nums, &double) -- [2, 4, 6, 8, 10] List.filter(nums, &over_three) -- [4, 5] List.reduce(nums, 0, &add) -- 15 List.first(nums) -- Some(1) List.reverse(nums) -- [5, 4, 3, 2, 1] ``` | Function | Signature | Description | | ---------- | ---------------------------------------- | -------------------------------- | | `length` | `(List[T]) -> Int` | Number of elements | | `map` | `(List[T], (T) -> U) -> List[U]` | Transform each element | | `filter` | `(List[T], (T) -> Bool) -> List[T]` | Keep elements matching predicate | | `reduce` | `(List[T], U, (U, T) -> U) -> U` | Fold left with accumulator | | `find` | `(List[T], (T) -> Bool) -> Option[T]` | First matching element | | `first` | `(List[T]) -> Option[T]` | First element | | `last` | `(List[T]) -> Option[T]` | Last element | | `head` | `(List[T]) -> Option[T]` | Alias for `first` | | `tail` | `(List[T]) -> List[T]` | All elements except the first | | `take` | `(List[T], Int) -> List[T]` | First N elements | | `drop` | `(List[T], Int) -> List[T]` | All except first N elements | | `sort` | `(List[T]) -> List[T]` | Sort in ascending order | | `sort_by` | `(List[T], (T) -> U) -> List[T]` | Sort by key function | | `reverse` | `(List[T]) -> List[T]` | Reverse order | | `flatten` | `(List[List[T]]) -> List[T]` | Flatten one level of nesting | | `concat` | `(List[T], List[T]) -> List[T]` | Join two lists | | `contains` | `(List[T], T) -> Bool` | Membership test | | `any` | `(List[T], (T) -> Bool) -> Bool` | True if any element matches | | `all` | `(List[T], (T) -> Bool) -> Bool` | True if all elements match | | `none` | `(List[T], (T) -> Bool) -> Bool` | True if no element matches | | `zip` | `(List[T], List[U]) -> List[[T, U]]` | Pair elements from two lists | | `uniq` | `(List[T]) -> List[T]` | Remove duplicates | | `count` | `(List[T], (T) -> Bool) -> Int` | Count matching elements | | `group_by` | `(List[T], (T) -> K) -> Map[K, List[T]]` | Group by key function | ## Map [Section titled “Map”](#map) Key-value operations. Maps are unordered collections of unique keys to values. ```skein let m = Map.put({}, "name", "Alice") Map.get(m, "name") -- Some("Alice") Map.keys(m) -- ["name"] Map.has(m, "email") -- false ``` Record literals like `{ name: "Alice", age: 30 }` compile with **atom** keys — they are payload values for typed fields, `respond.json`, and effect calls, and their keys do not match the string keys used by `Map.get`/`Map.has`. Build string-keyed maps with `Map.put` when you need keyed lookups. | Function | Signature | Description | | ------------ | --------------------------------------- | ------------------------------------- | | `get` | `(Map[K,V], K) -> Option[V]` | Lookup by key | | `put` | `(Map[K,V], K, V) -> Map[K,V]` | Insert or update key | | `delete` | `(Map[K,V], K) -> Map[K,V]` | Remove key | | `keys` | `(Map[K,V]) -> List[K]` | All keys | | `values` | `(Map[K,V]) -> List[V]` | All values | | `entries` | `(Map[K,V]) -> List[[K,V]]` | All key-value pairs | | `size` | `(Map[K,V]) -> Int` | Number of entries | | `has` | `(Map[K,V], K) -> Bool` | Key existence check | | `merge` | `(Map[K,V], Map[K,V]) -> Map[K,V]` | Merge maps (second wins on conflicts) | | `map_values` | `(Map[K,V], (V) -> U) -> Map[K,U]` | Transform all values | | `filter` | `(Map[K,V], (K,V) -> Bool) -> Map[K,V]` | Keep matching entries | There is no raising Map lookup. To read a key with a fallback, unwrap the Option: `Option.unwrap(Map.get(m, key), default)`. ## Set [Section titled “Set”](#set) Unique collections. Sets are unordered collections of unique values. ```skein let s = Set.from([1, 2, 3]) Set.contains(s, 2) -- true Set.union(s, Set.from([3, 4])) -- Set{1, 2, 3, 4} Set.size(s) -- 3 ``` | Function | Signature | Description | | -------------- | ---------------------------- | -------------------------------- | | `from` | `(List[T]) -> Set[T]` | Create set from list | | `add` | `(Set[T], T) -> Set[T]` | Add element | | `remove` | `(Set[T], T) -> Set[T]` | Remove element | | `contains` | `(Set[T], T) -> Bool` | Membership test | | `size` | `(Set[T]) -> Int` | Number of elements | | `union` | `(Set[T], Set[T]) -> Set[T]` | Union of two sets | | `intersection` | `(Set[T], Set[T]) -> Set[T]` | Intersection of two sets | | `difference` | `(Set[T], Set[T]) -> Set[T]` | Elements in first but not second | | `to_list` | `(Set[T]) -> List[T]` | Convert to list | ## Option [Section titled “Option”](#option) Optional values. Used when a value may or may not exist. Values are `Some(value)` or `None`. Functions like `List.find` and `Map.get` return `Option`. ```skein fn increment(n: Int) -> Int { n + 1 } let x = List.first([42]) -- Some(42) Option.unwrap(x, 0) -- 42 (returns 0 on None) Option.map(x, &increment) -- Some(43) Option.is_some(x) -- true ``` There is no `Some(...)`/`None` constructor syntax: Option values come from stdlib functions such as `List.first`, `List.find`, and `Map.get`. | Function | Signature | Description | | ---------- | -------------------------------------------- | --------------------------------------- | | `unwrap` | `(Option[T], T) -> T` | Extract value, or the default on `None` | | `map` | `(Option[T], (T) -> U) -> Option[U]` | Transform inner value if present | | `flat_map` | `(Option[T], (T) -> Option[U]) -> Option[U]` | Chain optional operations | | `is_some` | `(Option[T]) -> Bool` | True if `Some` | | `is_none` | `(Option[T]) -> Bool` | True if `None` | ## Result [Section titled “Result”](#result) Error handling. Used for operations that can succeed or fail. Values are `Ok(value)` or `Err(error)`. Effect calls and parsing functions return `Result`. ```skein fn increment(n: Int) -> Int { n + 1 } let r = Ok(42) Result.unwrap(r, 0) -- 42 (returns 0 on Err) Result.map(r, &increment) -- Ok(43) Result.is_ok(r) -- true ``` | Function | Signature | Description | | ---------- | -------------------------------------------------- | -------------------------------------- | | `unwrap` | `(Result[T,E], T) -> T` | Extract value, or the default on `Err` | | `map` | `(Result[T,E], (T) -> U) -> Result[U,E]` | Transform success value | | `map_err` | `(Result[T,E], (E) -> F) -> Result[T,F]` | Transform error value | | `flat_map` | `(Result[T,E], (T) -> Result[U,E]) -> Result[U,E]` | Chain fallible operations | | `is_ok` | `(Result[T,E]) -> Bool` | True if `Ok` | | `is_err` | `(Result[T,E]) -> Bool` | True if `Err` | | `ok` | `(T) -> Result[T,E]` | Wrap value in `Ok` | | `err` | `(E) -> Result[T,E]` | Wrap error in `Err` | ## Uuid [Section titled “Uuid”](#uuid) UUID parsing and formatting (v4). Generating a UUID is nondeterministic, so it is the capability-gated `uuid.new()` **effect** (requires `capability uuid`), not a stdlib function — see the Effects reference. ```skein let id = uuid.new() -- effect: "550e8400-e29b-41d4-a716-446655440000" Uuid.parse("...") -- stdlib: Ok(uuid) ``` | Function | Signature | Description | | ----------- | ---------------------------------- | ------------------------------ | | `parse` | `(String) -> Result[Uuid, String]` | Parse and validate UUID string | | `to_string` | `(Uuid) -> String` | Format UUID as string | ## Instant [Section titled “Instant”](#instant) Timestamps. Represents a point in time. Reading the current time is nondeterministic, so it is the capability-gated `instant.now()` **effect** (requires `capability instant`), not a stdlib function; the operations below are pure stdlib. ```skein let now = instant.now() -- effect (requires capability instant) let later = Instant.add(now, Duration.hours(2)) Instant.is_before(now, later) -- true Instant.diff(later, now) -- Duration (2 hours) ``` | Function | Signature | Description | | ----------- | ------------------------------------- | ------------------------------ | | `parse` | `(String) -> Result[Instant, String]` | Parse ISO 8601 string | | `to_string` | `(Instant) -> String` | Format as ISO 8601 | | `add` | `(Instant, Duration) -> Instant` | Add duration to instant | | `subtract` | `(Instant, Duration) -> Instant` | Subtract duration from instant | | `diff` | `(Instant, Instant) -> Duration` | Time between two instants | | `is_before` | `(Instant, Instant) -> Bool` | Chronological comparison | | `is_after` | `(Instant, Instant) -> Bool` | Chronological comparison | ## Duration [Section titled “Duration”](#duration) Time spans. Used with `Instant` for time arithmetic and with `timer.*` for scheduling. ```skein let d = Duration.minutes(30) Duration.to_seconds(d) -- 1800 Duration.to_string(d) -- "30m" ``` | Function | Signature | Description | | ------------ | ---------------------- | ------------------------ | | `seconds` | `(Int) -> Duration` | Create from seconds | | `minutes` | `(Int) -> Duration` | Create from minutes | | `hours` | `(Int) -> Duration` | Create from hours | | `days` | `(Int) -> Duration` | Create from days | | `to_seconds` | `(Duration) -> Int` | Convert to total seconds | | `to_string` | `(Duration) -> String` | Human-readable string | ## Compilation [Section titled “Compilation”](#compilation) Stdlib calls compile to static function calls on the corresponding runtime module: ```skein -- Skein source String.upcase("hello") -- Compiles to (Core Erlang) call 'Elixir.Skein.Runtime.Stdlib.String':'upcase'("hello") ``` The code generator recognizes stdlib calls by matching the module name against `@stdlib_modules` in the code generator. No capabilities are required for stdlib calls since they perform no I/O effects and are deterministic. The two nondeterministic generators — `uuid.new()` and `instant.now()` — are **not** stdlib; they are capability-gated effects so their values can be controlled under test and replay. # Agent-Writability Benchmark > How Skein measures its central pitch — an LLM agent writing new Skein from the docs, converging to a compiling program through structured diagnostics — and the measured trend over recorded live runs. Skein’s central pitch is that the language is co-optimized for humans to read and LLM agents to write (design principle P6). The agent-writability benchmark measures that instead of asserting it. ## The measured trend [Section titled “The measured trend”](#the-measured-trend) ![Agent-writability benchmark: tasks-green and first-try compile rates over recorded live runs](/Skein/writability-history.svg) Each point is one recorded live run of the full suite. The raw data lives in [`conformance/writability/history.jsonl`](https://github.com/kormie/Skein/blob/main/conformance/writability/history.jsonl); the chart regenerates whenever a live run is recorded. ## How it works [Section titled “How it works”](#how-it-works) For each of twelve fixed tasks spanning the language surface — pure functions, records with `Option`, `Result` flows, enums and `match`, stdlib callbacks, string interpolation, tools, HTTP handlers, the typed store, agents, LLM effects, and scenario capability environments — the harness runs a generate-compile-fix loop: 1. Ask a code-generating model for a complete Skein module. The generation context is the [agent primer](/Skein/reference/agent-primer/) that `skein new` scaffolds into every project as `AGENTS.md`; tasks whose surface the primer only sketches (tools, handlers, the typed store, `llm.chat`, scenarios) carry a short fixed crib of the relevant effect signatures, checked in with the suite. 2. Compile it and collect the structured diagnostics. 3. Mechanically apply every machine-applicable fix (`span` + `edit_kind` + `fix_code`), the way the LSP and MCP consumers do. 4. Feed the remaining diagnostics (JSON) back to the model and iterate to green, up to a cap. The report carries the P6 quality metrics: **first-try compile rate**, **mean iterations to green**, how much work the machine-applicable fixes did, and which diagnostics failed to converge. A non-converging diagnostic is treated as a compiler bug — the first live run surfaced several ([#336](https://github.com/kormie/Skein/issues/336)), and fixing them took the suite from 10/12 to 12/12 green. ## Deterministic by default [Section titled “Deterministic by default”](#deterministic-by-default) Every live run records the raw generations to `conformance/writability/recordings.json`. CI and release-readiness run in **replay mode**: the recorded generations re-run through the *current* compiler with no LLM calls, so the metrics are deterministic and a recorded solution that stops compiling is a caught regression, exactly like the dogfood corpus. ```bash # Replay the recordings (deterministic, no network): mix skein.bench # Re-measure live and refresh recordings + history + chart # (needs ANTHROPIC_API_KEY): mix skein.bench -- --live ``` A live run can also be triggered on demand from the repository’s **Writability Bench** GitHub Actions workflow; the refreshed recordings land through a normal pull request, pinned by the replay test. # Distribution > Packaging Skein so others can use it without the source repository. ## Current State [Section titled “Current State”](#current-state) Skein ships as **standalone binaries** via Burrito for Linux x86\_64, Linux ARM64, macOS x86\_64, and macOS ARM64, published automatically by CI on every `v*` tag (along with the VS Code extension `.vsix` and a checksums file). Users download a single `skein` binary and can immediately create, build, test, and run projects — no Erlang, Elixir, or Mix required. ```bash ./skein new my_project ./skein build my_project ./skein test my_project ./skein run my_project --port 4000 ``` `skein build` also supports writing compiled `.beam` files to disk: ```bash ./skein build my_project --output _build/beam ``` This page documents both what’s done and the remaining work to make Skein fully distributable. *** ## 1. Standalone CLI Binary ✅ [Section titled “1. Standalone CLI Binary ✅”](#1-standalone-cli-binary) A self-contained `skein` binary that bundles the compiler, runtime, and BEAM. ### Burrito Distribution (Implemented) [Section titled “Burrito Distribution (Implemented)”](#burrito-distribution-implemented) [Burrito](https://github.com/burrito-elixir/burrito) wraps an OTP release into a self-extracting archive for Linux, macOS, and Windows. It bundles the Erlang runtime so the user doesn’t need OTP installed. **Implementation:** * `burrito` (\~> 1.5) added as a dependency of `skein_cli` * Release configured in root `mix.exs` with four targets: Linux x86\_64, Linux ARM64, macOS x86\_64, macOS ARM64 * `Skein.CLI.Main` module implements the `Application` behaviour and dispatches subcommands via `Burrito.Util.Args.argv/0` * In release mode (Mix not available), the entry point reads args and routes to `compile`, `new`, `build`, `test`, `run`, `trace`, `agents`, `mcp`, `lsp`, `completions`, `version`, and `help` commands * In dev mode, the existing Mix aliases continue to work as before **Building standalone binaries:** ```bash # Build for the current platform MIX_ENV=prod mix release skein # Build for a specific target BURRITO_TARGET=linux MIX_ENV=prod mix release skein BURRITO_TARGET=macos_arm MIX_ENV=prod mix release skein ``` Binaries are written to `burrito_out/`. *** ## 2. OTP Releases for Skein Projects [Section titled “2. OTP Releases for Skein Projects”](#2-otp-releases-for-skein-projects) `skein build` compiles `.skein` files and can write `.beam` files to disk. To deploy a Skein project as a standalone server, the build step needs to produce a full OTP release. **Planned output structure:** ```plaintext _build/rel/my_service/ ├── bin/my_service # Start/stop script ├── lib/ │ ├── my_service-0.1.0/ # Compiled .beam files │ ├── skein_runtime-0.1.0/ │ └── ... └── releases/ └── 0.1.0/ ├── sys.config └── vm.args ``` **Steps:** 1. ~~Update `skein build` to write `.beam` files to disk~~ ✅ Done 2. Generate a minimal Mix project on the fly that depends on `skein_runtime` and includes the compiled modules 3. Run `mix release` against the generated project to produce a self-contained OTP release 4. ~~Support `skein build --output ./release`~~ ✅ Done ### Docker Images [Section titled “Docker Images”](#docker-images) Once OTP releases work, producing Docker images is straightforward: 1. Provide a `Dockerfile.skein` template in `skein new` scaffolding 2. Multi-stage build: compile in a builder image, copy the release into a minimal runtime image 3. The resulting image needs only the OS and ERTS — not Elixir, Mix, or the Skein compiler *** ## 3. Hex.pm Packages [Section titled “3. Hex.pm Packages”](#3-hexpm-packages) Publishing the Skein compiler and runtime to Hex.pm would allow Elixir developers to embed Skein compilation in their own projects (e.g., compiling `.skein` files at build time inside a Phoenix app). **Three packages:** | Package | Purpose | | ---------------- | ------------------------------------------------ | | `skein_compiler` | Lexer, parser, analyzer, codegen | | `skein_runtime` | Agent behaviour, HTTP, memory, LLM, store, trace | | `skein` | Meta-package pulling both plus CLI Mix tasks | **Steps:** 1. Add `package:` metadata (description, licenses, links) to each app’s `mix.exs` 2. Add `docs:` configuration for ExDoc 3. Ensure all dependencies are on Hex.pm (not Git-only) 4. Publish with `mix hex.publish` *** ## 4. GitHub Releases and CI ✅ [Section titled “4. GitHub Releases and CI ✅”](#4-github-releases-and-ci) Implemented: `.github/workflows/build.yml` triggers on version tags (`v*`), builds Burrito binaries for all four targets plus the VS Code `.vsix`, and publishes a GitHub Release with the artifacts, checksums, and auto-generated release notes. The tag step itself is automated too ([#100](https://github.com/kormie/Skein/issues/100)): a green version-bump merge to `main` tags, builds, and publishes the release with no manual steps. *** ## 5. Installer Script ✅ [Section titled “5. Installer Script ✅”](#5-installer-script) Implemented (shipped in v0.2.0): ```bash curl -fsSL https://kormie.github.io/Skein/install.sh | sh ``` The installer detects the user’s OS/architecture, downloads the matching binary from the latest GitHub release, verifies its SHA-256 against the release’s `checksums.txt`, and installs it to `~/.local/bin`. `SKEIN_VERSION` pins a version; `SKEIN_BIN_DIR` overrides the install directory. *** ## Priority Order [Section titled “Priority Order”](#priority-order) | Priority | Artifact | Status | | -------- | ------------------------------------ | --------------------------------------------------------------------------------------------- | | 1 | `skein build` writes `.beam` to disk | ✅ Done | | 2 | Burrito standalone binaries | ✅ Done (Linux x86\_64/ARM64, macOS x86\_64/ARM64) | | 3 | CI release pipeline | ✅ Done (binaries + `.vsix` + checksums on every `v*` tag; green version-bump merges auto-tag) | | 4 | Installer script | ✅ Done (`install.sh` served from the docs site, sha256-verified) | | 5 | OTP release generation | Enables standalone server deployment | | 6 | Hex.pm packages | Enables embedding Skein in Elixir projects | | 7 | Docker template | Enables container-based deployment | ## Prerequisites [Section titled “Prerequisites”](#prerequisites) All prerequisites for distribution work have been completed: * ✅ **Enum variant matching** — Enum variants compile to tagged tuples and pattern matching correctly destructures them * ✅ **Supervisor declarations** — Supervisors with `child`, `strategy:`, and `max_restarts:` are parsed, analyzed, and generate metadata * ✅ **`skein build --output`** — Writes compiled `.beam` files to a target directory # Roadmap > Current project status, what's been built, and prioritized next steps. Skein’s compilation pipeline, runtime, standard library, editor tooling, and distribution packaging are all functional. This page covers the current state, forward-looking priorities, and a history of completed work. The canonical, more detailed version lives in [`docs/ROADMAP.md`](https://github.com/kormie/Skein/blob/main/docs/ROADMAP.md). ## Current State [Section titled “Current State”](#current-state) The end-to-end pipeline works: `.skein` source files lex, parse, analyze, generate Core Erlang, compile to BEAM bytecode, and execute on OTP. The runtime supports agents, HTTP handlers, queue/schedule/topic handlers, typed store tables (schema-checked ETS, C5 [#255](https://github.com/kormie/Skein/issues/255)), LLM integration (production Anthropic backend with streaming, AWS Bedrock, and an OpenAI-compatible backend for local models), tool calling, memory, tracing, replayable event sourcing with opt-in SQLite persistence, real OTP supervision from `supervisor` declarations, and guard expressions in match arms. The full test suite (unit, property-based, and integration) runs green in CI on every change — see the CI badge on the README for current totals; counts aren’t tracked here because they grow with every change. **Sixteen example programs** (thirteen single-file plus the multi-file `market_research` project and its single-file variant) compile and run warning-free, covering HTTP APIs, agents with LLM and tools, queue workers, pub/sub notifications, background tasks, audit logging, semantic search, and a conversational assistant. All are covered by integration tests. ## Release Status [Section titled “Release Status”](#release-status) The release train so far: **v0.2.0** and **v0.3.0** shipped 2026-06-11, **v1.0.0-rc.1** was tagged 2026-06-12, and both **v0.4.0 — Truth & Soundness** and **v0.5.0 — Runtime Contract & Dogfood** shipped 2026-07-02. **A 2026-06-15 roadmap reset re-sequenced the path to 1.0.** A source-verified dogfooding audit of the skein-testing and FablePool ports reclassified GA from a docs-accuracy cleanup into a soundness + honesty + observability + conformance gate. **The active gate is v1.0.0-rc.5 — True release candidate** (the Wave F freeze: executable gates for every frozen surface, the agent-writability benchmark, and live-backend verification — [#332](https://github.com/kormie/Skein/issues/332)/[#320](https://github.com/kormie/Skein/issues/320)/[#334](https://github.com/kormie/Skein/issues/334)); GA follows only after the RC soaks and is **not imminent**. (Tags rc.2–rc.4 exist from the pre-reset June train, so the next RC tag is rc.5.) *** ## What’s Next [Section titled “What’s Next”](#whats-next) The path to 1.0 runs through the contract-first waves: **v0.4.0 — Truth & Soundness** (shipped 2026-07-02), then **v0.5.0 — Runtime Contract & Dogfood** (shipped 2026-07-02), and now the true RC — **v1.0.0-rc.5**, the Wave F freeze — then GA after the candidate soaks, which is **not imminent**. The canonical detail (waves, acceptance criteria, citations) lives in [`docs/ROADMAP.md`](https://github.com/kormie/Skein/blob/main/docs/ROADMAP.md). ### v0.4.0 — Truth & Soundness (complete) [Section titled “v0.4.0 — Truth & Soundness (complete)”](#v040--truth--soundness-complete) Wave A (truth reset: honest docs/spec/stability framing, surface cuts like tool `policy` blocks) and Wave B (analyzer/codegen soundness, B1–B6) landed — Wave B completed 2026-07-01 and was source-verified by the 2026-07-02 sanity check, with its residual holes (#309–#311, #313, #318, #319) closed. `?` truly early-returns, unknown/widened types cannot cross declared boundaries (E0037), call arguments are type-checked everywhere, the analyzer-accept ⇒ BEAM-load bridge holds, records are nominal, and tool/provider bodies are checked against their contracts (E0038). ### v0.5.0 — Runtime Contract & Dogfood (shipped 2026-07-02) [Section titled “v0.5.0 — Runtime Contract & Dogfood (shipped 2026-07-02)”](#v050--runtime-contract--dogfood-shipped-2026-07-02) Wave C made the runtime expose exactly the contract the analyzer and spec claim, plus Wave D’s continuous dogfood gate — all landed 2026-07-02: * **C1** — authoritative effect-ABI registry (`Skein.EffectABI`, [#296](https://github.com/kormie/Skein/issues/296)): one source of truth for effect signatures, store methods, provider contracts, and error enums; analyzer/spec/runtime drift is a CI failure * **C2** — one structured-error ABI ([#297](https://github.com/kormie/Skein/issues/297)): LLM/tool/provider/store/HTTP errors are matchable nominal enum variants (`Err(LlmError.RateLimit(ms))`), not raw Elixir structs; the variant registry is frozen * **C3** — one recursive schema engine ([#298](https://github.com/kormie/Skein/issues/298)): `req.json[T]`, `llm.json[T]`, and tool input *and* output all validate through the same recursive validator (`Skein.Runtime.JsonSchema`) * **C5** — typed store tables ([#255](https://github.com/kormie/Skein/issues/255)): `capability store.table("games", Game)` names a record type; store methods type as `Result[T, StoreError]` and writes are schema-checked at runtime — landed on the ETS store compiled programs actually use, not the unwired Ecto path * **C6** — EventStore persistence ([#299](https://github.com/kormie/Skein/issues/299)): the SQLite backend is wired onto the ordinary append path — opt-in via `Persistence.enable/1`, enabled by default by `skein run` (`.skein/events.db`, `--no-persist` opts out), with persisted history reloaded on restart; the persisted shapes freeze at Wave F * **#325** — `supervisor` declarations boot real OTP supervisors under `skein run` ([#325](https://github.com/kormie/Skein/issues/325)) * **Wave D / [#262](https://github.com/kormie/Skein/issues/262)** — the continuous dogfood conformance gate: compile + load + **run** reduced, pinned programs from the checked-in dungeon and fablepool ports on every change ### v1.0.0-rc.5 and GA [Section titled “v1.0.0-rc.5 and GA”](#v100-rc5-and-ga) The Wave F freeze **landed 2026-07-02**: grammar/keywords, the diagnostics registry, the full effect ABI + error shapes, schema derivation, the CLI/JSON/config surface, compiled-metadata classes, and the EventStore persisted shapes are all declared frozen with executable drift gates against the vectors under `conformance/freeze/` ([#332](https://github.com/kormie/Skein/issues/332)), agent-writability is measured rather than asserted ([#320](https://github.com/kormie/Skein/issues/320) — see the [writability benchmark](/Skein/reference/writability-benchmark/)), and `llm.stream` was live-verified against the production backend ([#334](https://github.com/kormie/Skein/issues/334)). What remains is release mechanics: cut the rc.5 tag, soak the candidate, then promote to GA. The canonical-substrate question was resolved out of 1.0 ([#300](https://github.com/kormie/Skein/issues/300) closed as Alternative B): the substrate items live in v1.1, and there is no v0.6.0 milestone. ### Post-1.0 Backlog [Section titled “Post-1.0 Backlog”](#post-10-backlog) Tracked in [`docs/ROADMAP.md`](https://github.com/kormie/Skein/blob/main/docs/ROADMAP.md) with linked issues: * **v1.1: Hardening & Language** — closures (#248), effectful crypto (#257), content-addressed store tables (#333), language ergonomics (#251/#249/#247), `via` if still useful, `llm.rerank` (#145), docs/spec drift guards (#202), LSP rename/references (#240) * **v1.2: Interop & Agent Workflows** — Erlang/Elixir FFI (`extern`, #141), human-in-the-loop approval workflows (#144), web trace viewer (#143), Raxol CLI TUI (#171, separately gated), structural codemod (#241) * **Future: Platform** — hot code upgrades (#142), managed deployment platform (#148), tool/connector marketplace (#149) *** ## Completed Work [Section titled “Completed Work”](#completed-work) Everything below is implemented and tested. ### Core Pipeline (Phases 1–7) [Section titled “Core Pipeline (Phases 1–7)”](#core-pipeline-phases-17) | Phase | Name | Summary | | ----- | ---------------------- | ---------------------------------------------------------------------------------------------- | | 1 | Hello BEAM | End-to-end compilation: lexer → parser → analyzer → Core Erlang → BEAM | | 2 | Type System | Type checking, schemas, constraint annotations, `Option[T]`, `Result[T, E]`, `!`/`?` operators | | 3 | Capabilities & Effects | Capability declarations, compile-time checking (modules + agents), HTTP client, tracing | | 4 | Handlers & HTTP | HTTP handlers with routing, request dispatch, Bandit + Plug server | | 5 | Storage | ETS-backed `store.table` with get, put, delete, query | | 6 | Agents | Agent state machines, phases, transitions, memory, LLM, tools, events | | 7 | Testing & CLI | Test constructs, full CLI (new, build, test, run, trace) | ### Infrastructure (Phase 8) [Section titled “Infrastructure (Phase 8)”](#infrastructure-phase-8) | Sub-phase | Name | Summary | | --------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 8a | Test Infrastructure | Scenario tests (`given`/`expect`), golden trace tests, replay engine (trace loading + memory rebuild) | | 8b | Storage Backend | Ecto schema generation, migrations, SQLite via `ecto_sqlite3` (library code only — typed tables landed on the ETS store instead, C5 #255; EventStore persistence uses its own SQLite backend, #299) | | 8c | HTTP Server | Bandit + Plug integration, `req.json[T]` body validation | | 8d | Canonical Examples | 14 working programs with integration tests | | 8e | Queue & Schedule | `handler queue` and `handler schedule` constructs | | 8f | LLM Streaming | `llm.stream` with chunked responses and trace spans | ### Type System & Schemas [Section titled “Type System & Schemas”](#type-system--schemas) * Field access resolves through user-defined type declarations; `user.email + 42` is a type error (E0020) * Pattern bindings carry the inner types of `Result`, `Option`, and enum variants * Nested user types generate fully resolved JSON Schema; enum variants produce `oneOf`; `Map[K, V]` generates `additionalProperties`; circular references are safe ### LLM Integration [Section titled “LLM Integration”](#llm-integration) * Production **Anthropic backend** (Messages API): `chat`, `json` (schema-in-system-prompt), `stream` (SSE), retry on 429, structured errors, API-key redaction * Model-scoped runtime capability checks: `llm.chat("model-x", ...)` without a matching `capability model(...)` is blocked * Current model IDs throughout examples and docs ### Hardening [Section titled “Hardening”](#hardening) | Item | Summary | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Float division codegen | `/` uses Erlang `/` for floats, `div` for integers | | String-literal match patterns | `match s { "approve" -> ... }` compiles to proper binary patterns | | `state.field` everywhere | Agent state access works in nested expression positions | | Postfix `!`/`?` unwrap | `store.users.get(id)!` parses as unwrap-of-call and the chain continues (likewise `?`); the pre-paren `get!(id)` spelling was removed (#268) | | Contextual keywords | `input`, `output`, `state`, etc. no longer globally reserved | | Multiple `emit` per handler | All events accumulated, not just the last | | Agent instance-scoped memory | Keys scoped as `{agent}:{instance}:{key}`; concurrent instances don’t collide | | Tool input validation | Inputs validated against the tool’s JSON Schema; `validation_error` on mismatch | | Capability naming | `queue.consume` / `schedule.trigger` (old names get a targeted rename hint) | | Cross-module `tool.call` | `implement` blocks compile to callable entry points; tools registered at module load (v0.1.5) | | Variant construction | `Ok(x)`, `Err(e)`, `Event.Charge(n)`, `ErrName.from(cause)`, and zero-field forms (`Status.Active`, bare `Active`) all compile in expression position; unknown variants and wrong arity are structured errors | | Spec section 8 sweep | Every section-8 example compiles with zero diagnostics (pinned by `spec_examples_test.exs`) | | zsh tab-completion | `skein completions zsh` (drift-tested against the help text) | | MCP compile\_check fidelity | Warnings included (`Compiler.check_file/1`); project mode checks `src/` + `test/` like `skein test` | | Structured assertion failures | Failing asserts report operands, rendered expression, and `file:line` (Skein.Runtime.AssertionError) | | `skein new` git init | Repo + baseline `.gitignore` scaffolded by default (`--no-git` to skip; never nests inside an existing work tree) | | Agent events to EventStore | `emit` flushes durably as `:user_event` (agent/instance/phase tags); crash-safe | | Schedule auto-firing | Cron tick with per-minute dedup; Server registers schedule handlers; `tick_at/1` for deterministic tests | | Capability checks in test blocks | Effects inside `test`/`scenario`/`golden` require capabilities (E0012) and count as usage (no scaffold W0002) | | Named arguments in calls | `f(name: value)` for local fns and documented effect signatures; analyzer rewrites to positional order at compile time (E0026 on misuse) | | Agent nesting inside modules | `module Foo { agent Bar }` → `Skein.Agent.Foo.Bar`; module types and capabilities apply to the nested agent | | Types usable from agents | Module types visible to nested agents; derived JSON Schema flows into `llm.json[T]` from agent handlers | | EventStore | In-memory, size-bounded ETS event log with opt-in SQLite persistence on the ordinary append path (#299): `skein run` enables it by default and reloads persisted history on restart; shapes freeze at Wave F | | Error system | Structured error/warning codes (spec §7); `context` and `fix_code` populated on all analyzer/parser/lexer errors | | Replay backend injection | Recorded traces intercept `llm`/`http`/`tool` effects on replay; exhausted or mismatched traces are structured errors, never silent live calls (#73) | | Scoped capability labels | `memory.kv`/`event.log`/`process.spawn`/`timer` capability params are scope labels the compiler threads into runtime calls and the runtime enforces exactly (#69, #57) | | Task bodies | `process.spawn("name", &fn)` and `timer.after`/`timer.interval` work arguments run in supervised tasks (#74, #155) | | Local LLM backends | OpenAI-compatible backend + `[env..llm]` profiles in `skein.toml` with `model_map` for Ollama/LM Studio/vLLM dev loops (#107) | | AWS Bedrock backend | Converse API with SigV4 signing, `region` + `model_map` config (#173) | | Match guards | `pattern if expr ->` with a guard-safe operator subset (#147) | | Value-level exhaustiveness | W0004 warns when a variant is covered only by literal field patterns (#76) | | LSP code actions | Editor quickfixes built from each diagnostic’s `fix_hint`/`fix_code` (#108) | | `llm.embed` backend | Embeddings served by the OpenAI-compatible backend’s `/embeddings` endpoint (#146) | ### Standard Library [Section titled “Standard Library”](#standard-library) 11 modules: | Module | Purpose | | --------------- | ---------------------------------------------------------------------------------------------------- | | `String` | Manipulation, search, splitting, formatting | | `Int` / `Float` | Arithmetic, parsing, conversion | | `List` | Functional operations (map, filter, reduce, etc.) | | `Map` | Key-value operations | | `Set` | Mathematical set operations | | `Option` | `Some(T)` / `None` handling | | `Result` | `Ok(T)` / `Err(E)` handling | | `Uuid` | UUID parsing/formatting (generation is the capability-gated `uuid.new()` effect, not ambient stdlib) | | `Instant` | ISO 8601 timestamps | | `Duration` | Time intervals | ### Additional Capabilities [Section titled “Additional Capabilities”](#additional-capabilities) | Feature | Description | | ----------------------------- | --------------------------------------------------------------------- | | Topic pub/sub | `handler topic` + `topic.publish` effect (name-scoped runtime checks) | | Idempotency | `idempotent(key)` with TTL-based deduplication | | Process spawning | `process.spawn` with DynamicSupervisor | | Timers | `timer.after`, `timer.interval`, `timer.cancel` | | Event sourcing | Unified `EventStore` with query, memory rebuild from events | | `suspend`/`resume` | Agent lifecycle management | | `respond.text`/`respond.html` | Content-type response variants | | `trace.annotate` | Custom annotations in the event stream | | `llm.embed` | Embedding vectors (requires an embeddings-capable backend) | ### Editor Tooling [Section titled “Editor Tooling”](#editor-tooling) * **VS Code extension** with TextMate grammar, 30+ snippets, LSP client * **Language Server** (GenLSP): diagnostics, document symbols, hover, go-to-definition, completions, semantic tokens — with request/response integration tests ### Distribution [Section titled “Distribution”](#distribution) * **Burrito binaries** for Linux x86\_64/ARM64 and macOS x86\_64/ARM64 * **GitHub Release automation** — binaries published automatically on `v*` tags * **Auto-tagged releases** — a green version-bump merge to `main` tags, builds, and publishes the release with no manual steps; each release carries a docs snapshot (incl. `llms*.txt`) * `skein build --output` writes `.beam` files to disk * See [Distribution](/Skein/roadmap/distribution/) for remaining packaging work # Agents > How Skein agents compile to gen_statem state machines and run on the BEAM. ## Overview [Section titled “Overview”](#overview) Agents are the core construct in Skein for building stateful, phase-driven services. Each agent is a state machine with: * A **Phase** enum defining valid states and transitions * **State** fields for persistent data across phases * An **`on start`** handler that initializes the agent * **`on phase`** handlers that execute when entering each phase * **Capabilities** for memory, LLM, HTTP, and other effects At runtime, agents compile to `:gen_statem` processes managed by `Skein.Runtime.Agent`. ## Skein Source [Section titled “Skein Source”](#skein-source) ```skein agent RefundBot { capability model("anthropic", "claude-opus-4-8") capability memory.kv("sessions") state { request_id: String amount: Int } enum Phase { Review -> [Approved, Denied] Approved -> [Done] Denied -> [Done] Done -> [] } on start(request_id: String, amount: Int) -> { memory.put("request_id", request_id) memory.put("amount", amount) transition(Phase.Review) } on phase(Phase.Review) -> { let amount = memory.get("amount")! let decision = llm.chat("claude-opus-4-8", "Evaluate refund. Reply approve or deny.", amount) match decision { Ok("approve") -> transition(Phase.Approved) _ -> transition(Phase.Denied) } } on phase(Phase.Approved) -> { let amount = memory.get("amount")! emit RefundApproved { amount: amount } transition(Phase.Done) } on phase(Phase.Denied) -> { let amount = memory.get("amount")! emit RefundDenied { amount: amount } transition(Phase.Done) } on phase(Phase.Done) -> { stop() } } ``` ## Compilation [Section titled “Compilation”](#compilation) The compiler processes agents through all four analyzer passes: 1. **Name resolution** — registers the agent, its Phase enum variants, state fields, and handlers 2. **Type checking** — validates state field types and handler expressions 3. **Capability checking** — verifies `memory.kv`, `model`, `http.out`, `store.table` capabilities against effect calls in handlers 4. **Transition checking** — validates that every `transition(Phase.X)` call targets a phase reachable from the current handler’s phase via the `->` declarations ### Generated Module [Section titled “Generated Module”](#generated-module) An agent named `RefundBot` compiles to `Elixir.Skein.Agent.RefundBot` with these functions: | Function | Purpose | | --------------------- | --------------------------------------------------------- | | `start_link/1` | Start the agent with initial params | | `__phases__/0` | Return phase metadata (variants and valid transitions) | | `__capabilities__/0` | Return declared capabilities as a list of maps | | `__start_handler__/2` | Execute the `on start(...)` handler | | `__phase_handler__/3` | Execute phase-specific handlers, dispatched by phase atom | ### Nesting Inside Modules [Section titled “Nesting Inside Modules”](#nesting-inside-modules) Agents can be declared inside a module (the spec section 8.4 shape): ```skein module RefundService { capability model("anthropic", "claude-opus-4-8") type RefundDecision { action: String } agent RefundAgent { capability memory.kv("refund_sessions") -- ... } } ``` A nested agent compiles to its own BEAM module namespaced under the parent (`Skein.Agent.RefundService.RefundAgent`) alongside the module’s (`Skein.User.RefundService`). It sees the module’s `type` declarations (so `llm.json[RefundDecision]` works in handlers) and the module’s capabilities apply to it in addition to its own. Top-level agents (one per file) work unchanged — `examples/market_research/` ships both shapes. Agents never declare `type` blocks of their own — nesting is *the* way to use named types from an agent. The derived JSON Schema flows into `llm.json[T]` requests made from nested agent handlers. ### Transition Validation (E0030) [Section titled “Transition Validation (E0030)”](#transition-validation-e0030) The analyzer checks transition validity at compile time. Given the Phase enum: ```skein enum Phase { Review -> [Approved, Denied] Approved -> [Done] Denied -> [Done] Done -> [] } ``` A `transition(Phase.Done)` call inside `on phase(Phase.Review)` would be a compile error — `Review` can only transition to `Approved` or `Denied`. ## Runtime [Section titled “Runtime”](#runtime) ### Process Lifecycle [Section titled “Process Lifecycle”](#process-lifecycle) `Skein.Runtime.Agent` manages the `:gen_statem` lifecycle: ```plaintext start_link(args) │ ▼ init: call __start_handler__(args, state) │ ├── {:transition, phase, state, events} │ → move to phase, queue :execute_phase │ ├── {:stop, state, events} │ → terminate normally │ └── {:keep, state, events} → stay in :__idle__ state ``` When `:execute_phase` fires: ```plaintext handle_event(:internal, :execute_phase, phase, data) │ ▼ call __phase_handler__(phase, state, events) │ ├── {:transition, next_phase, state, events} │ → move to next_phase, queue :execute_phase again │ ├── {:stop, state, events} │ → terminate normally │ └── {:keep, state, events} → stay in current phase, wait for external events ``` ### Querying Agent State [Section titled “Querying Agent State”](#querying-agent-state) The query API works on agents that are still alive — one waiting in a phase (a handler that returns `keep`) or suspended for human review: ```elixir # refund_agent.skein suspends on Phase.Failed instead of stopping, # so the process stays alive and can be inspected: Skein.Runtime.Agent.get_phase(pid) #=> :failed Skein.Runtime.Agent.get_state(pid) #=> %{} (this agent persists its data via memory.kv, not state fields) Skein.Runtime.Agent.is_suspended?(pid) #=> true ``` RefundBot above is different: it runs straight through to `Phase.Done`, whose handler calls `stop()`, so the process terminates on its own — calling `get_phase/1` on it exits with `:noproc`. Terminated agents leave their trail in the EventStore instead: every `emit` is flushed as a `:user_event` tagged with the agent name, instance id, and phase: ```elixir {:ok, _pid} = Skein.Agent.RefundBot.start_link(%{request_id: "abc", amount: 100}) Skein.Runtime.EventStore.query(kind: :user_event) #=> [%{kind: :user_event, event: "RefundDenied", data: %{amount: 100}, # agent: "RefundBot", instance_id: "...", phase: :denied, ...}] ``` `start_link/1` takes a map keyed by the `on start(...)` parameter names. ### Handler Return Values [Section titled “Handler Return Values”](#handler-return-values) Agent handlers communicate via tagged tuples: | Return | Meaning | | ------------------------------------- | --------------------------------------------------------------------------- | | `{:transition, phase, state, events}` | Move to `phase`, merge `state`, append `events`, then execute phase handler | | `{:suspend, reason, state, events}` | Pause the agent with a reason string; agent enters `:suspended` state | | `{:stop, state, events}` | Terminate the agent normally | | `{:keep, state, events}` | Stay in current phase, merge state and events | The code generator maps Skein constructs to these tuples: * `transition(Phase.Review)` → `{:transition, :review, state_updates, new_events}` * `suspend(reason)` → `{:suspend, reason_string, state_updates, new_events}` * `stop()` → `{:stop, state_updates, new_events}` * `emit EventName { field: value }` → appends `%{event: "EventName", field: value}` to the events list **and** flushes to the EventStore as a `:user_event` (tagged with agent name, instance id, and phase) after the handler completes — events emitted before a crash survive, and `EventStore.query(kind: :user_event)` sees them ### Suspending and Resuming [Section titled “Suspending and Resuming”](#suspending-and-resuming) When a handler returns `{:suspend, reason, state, events}`, the agent enters the `:suspended` state. The agent process stays alive but does not execute any phase handlers. See [Language > Agents: Suspending](/Skein/language/agents/#suspending-and-resuming) for the Skein syntax. **Runtime API for suspended agents:** ```elixir # Check if an agent is suspended Skein.Runtime.Agent.is_suspended?(pid) #=> true # Get the suspension reason Skein.Runtime.Agent.get_suspend_reason(pid) #=> "Requires human review" # Resume the agent to a specific phase Skein.Runtime.Agent.resume(pid, :done) #=> :ok ``` Resume transitions the agent from `:suspended` to the specified phase and executes the phase handler. If the agent is not suspended, `resume/2` returns `{:error, :not_suspended}`. ## Memory Integration [Section titled “Memory Integration”](#memory-integration) Agents use `memory.kv` for scoped key-value storage that persists across phase transitions: ```skein agent SessionTracker { capability memory.kv("sessions") enum Phase { Active -> [] } on start(session_id: String, data: String) -> { memory.put(session_id, data) memory.put("current", session_id) transition(Phase.Active) } on phase(Phase.Active) -> { let session_id = memory.get("current")! let data = memory.get(session_id)! trace.annotate("session_data", data) stop() } } ``` The namespace comes from the scoped `capability memory.kv(namespace)` declaration (at most one per agent) — call sites pass only the key, and the compiler threads the namespace into every generated runtime call. Memory is backed by a single shared ETS table (`:skein_memory`) keyed by `{namespace, key}` — namespaces are never separate tables. Inside agents, keys are additionally prefixed with the agent name and instance id, so concurrent instances never collide. ## LLM Integration [Section titled “LLM Integration”](#llm-integration) Agents can call LLM models for decision-making. Schema-constrained `llm.json[T]` needs a named type, and agents never declare `type` blocks of their own — so an agent that wants structured output is nested inside the module that declares the type (see [Nesting Inside Modules](#nesting-inside-modules) above): ```skein module ClassifyService { capability model("anthropic", "claude-opus-4-8") type Decision { action: String } agent Classifier { capability memory.kv("classifier") enum Phase { Classify -> [] } on start(text: String) -> { memory.put("text", text) transition(Phase.Classify) } on phase(Phase.Classify) -> { let text = memory.get("text")! -- Unstructured text response let analysis = llm.chat("claude-opus-4-8", "Classify this input", text) trace.annotate("analysis", analysis) -- Schema-constrained JSON response (uses the module's Decision type) let decision = llm.json[Decision]( model: "claude-opus-4-8", system: "Decide the action. Return JSON.", input: text ) memory.put("decision", decision) stop() } } } ``` The LLM client uses a pluggable backend system. In tests, `Skein.Runtime.Llm.TestBackend` returns deterministic responses. In production, calls are served by the shipped `AnthropicBackend`, `OpenAiCompatibleBackend`, or `BedrockBackend` (selected via the `[llm]` profile in skein.toml); custom backends implement the `Skein.Runtime.Llm.Backend` behaviour. # Local Models for Development > Serve llm.chat/llm.json from a local model server (oMLX, Ollama, LM Studio, vLLM) in dev — zero source edits, capabilities untouched. Testing agents against Anthropic means real inference spend. A local model server can serve dev traffic for free — and in Skein, switching backends never touches source: `capability model("anthropic", "claude-opus-4-8")` stays the code’s contract whether traffic goes to Anthropic or `localhost`. ## Environment profiles in skein.toml [Section titled “Environment profiles in skein.toml”](#environment-profiles-in-skeintoml) The `[llm]` section selects the production backend; `[env..llm]` sections override it per environment: ```toml [llm] # default: production backend = "anthropic" [env.dev.llm] backend = "openai_compatible" base_url = "http://localhost:10240/v1" api_key_env = "OMLX_API_KEY" # optional; most local servers need none model_map = { "claude-opus-4-8" = "mlx-community/Qwen3-30B" } ``` `model_map` remaps the model named in the capability declaration to whatever the local server hosts. Unmapped models pass through unchanged. ## Selecting an environment [Section titled “Selecting an environment”](#selecting-an-environment) `skein run` and `skein test` resolve the active environment from the `--env` flag, then the `SKEIN_ENV` environment variable: ```bash # Dev traffic served by the local server — zero source edits SKEIN_ENV=dev skein test skein test --env dev # Production: Anthropic, exactly as declared skein run ``` ## The OpenAI-compatible backend [Section titled “The OpenAI-compatible backend”](#the-openai-compatible-backend) `backend = "openai_compatible"` speaks `POST {base_url}/chat/completions` — the de facto local standard served by oMLX, Ollama, LM Studio, llama.cpp, and vLLM. Details: * `llm.json` injects the schema into the system prompt (the one approach every local server supports) and strips markdown fences from the reply * `llm.embed` calls `POST {base_url}/embeddings` * `llm.stream` performs a regular completion returned as a single chunk (SSE framing varies too much across local servers) * A server that is down produces a structured `LlmError` naming the `base_url` — never a crash Capability checks and tracing are unchanged: every call is still checked against the declared `model(...)` capability, and each llm trace span records which `backend` and `base_url` served it, so a trace never leaves you guessing whether you burned tokens. ## Embeddings [Section titled “Embeddings”](#embeddings) Anthropic has no embeddings API, so `llm.embed` always runs through an OpenAI-compatible `/embeddings` endpoint. Two setups cover development and production: ```toml # Development: a local embedding model (Ollama, LM Studio, vLLM, ...) [env.dev.llm] backend = "openai_compatible" base_url = "http://localhost:11434/v1" model_map = { "voyage-3-large" = "nomic-embed-text" } # Production: Voyage AI (Anthropic's recommended embeddings provider) — # its /embeddings endpoint speaks the same OpenAI-compatible shape. [env.prod.llm] backend = "openai_compatible" base_url = "https://api.voyageai.com/v1" api_key_env = "VOYAGE_API_KEY" ``` The source and capability declarations never change: declare `capability model("voyage", "voyage-3-large")`, call `llm.embed("voyage-3-large", text)`, and `model_map` resolves the model per environment. `examples/semantic_search.skein` is the runnable end-to-end example. Embed spans record `backend`/`base_url` like chat spans, and a server that is down is a structured `LlmError`. ## Backends [Section titled “Backends”](#backends) | `backend` | Serves calls via | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `anthropic` | Anthropic Messages API (production default) | | `bedrock` | Amazon Bedrock Converse API — SigV4-signed with credentials from the AWS env vars or the AWS credential chain (`AWS_PROFILE` files, IAM Identity Center / SSO via `aws sso login`, EKS IRSA, ECS task roles, EC2 instance metadata; cached and auto-refreshed); set `region` (or `AWS_REGION`) and `model_map` to remap declared models to Bedrock model IDs (e.g. `"global.anthropic.claude-sonnet-4-6"`) | | `openai_compatible` | `POST {base_url}/chat/completions` on a local/self-hosted server | | `test` | Deterministic in-process responses (CI without inference) | ## Example: oMLX on Apple Silicon [Section titled “Example: oMLX on Apple Silicon”](#example-omlx-on-apple-silicon) ```toml [env.dev.llm] backend = "openai_compatible" base_url = "http://localhost:10240/v1" model_map = { "claude-opus-4-8" = "mlx-community/Qwen3-30B" } ``` ```bash SKEIN_ENV=dev skein test # local, free skein run # Anthropic, as declared ``` # Runtime > The Skein runtime library -- agents, HTTP client, handler dispatch, store, memory, LLM client, queue/schedule dispatch, capability enforcement, trace recording, and HTTP server. ## Overview [Section titled “Overview”](#overview) The Skein runtime (`skein_runtime`) provides the libraries that compiled Skein code calls at execution time. It handles: * **Agent runtime** — state machines for Skein agents via `Skein.Runtime.Agent`, built directly on OTP’s `:gen_statem` behaviour * **HTTP client** — making outbound HTTP requests via `Skein.Runtime.Http` * **Capability enforcement** — validating effect calls against declared capabilities * **Handler dispatch** — routing HTTP requests to compiled handler functions * **Queue dispatch** — subscribing to named queues and dispatching messages via `Skein.Runtime.Queue` * **Schedule dispatch** — cron-based scheduling and handler triggering via `Skein.Runtime.Schedule` * **Store** — typed, schema-checked ETS-backed storage with capability enforcement (the Ecto/SQLite path is unwired library code) * **Memory** — scoped KV storage with namespace isolation via `Skein.Runtime.Memory` * **LLM client** — provider-agnostic LLM calls with schema-constrained JSON via `Skein.Runtime.Llm` * **HTTP server** — Bandit + Plug HTTP server for serving Skein handlers * **Unified event store** — a single append-only event log for all trace spans, user events, memory state changes, and annotations via `Skein.Runtime.EventStore` The runtime is an OTP application that starts automatically when Skein code is loaded. ## Runtime Modules [Section titled “Runtime Modules”](#runtime-modules) ### `Skein.Runtime.Agent` [Section titled “Skein.Runtime.Agent”](#skeinruntimeagent) Runtime support for Skein agent state machines. Wraps `:gen_statem` to manage the agent lifecycle — starting agents, executing phase handlers, processing transitions, and recording events. **API:** ```elixir Skein.Runtime.Agent.start_link(module, args) #=> {:ok, pid} Skein.Runtime.Agent.get_phase(pid) #=> :review # State always starts empty — start params are passed to the on start # handler but never copied into state; handlers populate memory instead. Skein.Runtime.Agent.get_state(pid) #=> %{} # Events are keyed :event with the PascalCase event name from `emit`. Skein.Runtime.Agent.get_events(pid) #=> [%{event: "RefundApproved", amount: 100}] ``` **How it works:** 1. `start_link/2` starts a `:gen_statem` process with the agent module and initial args 2. The `init` callback calls the module’s `__start_handler__/2` to run the `on start` handler 3. `transition(Phase)` in a handler returns `{:transition, phase, state, events}`, which moves the gen\_statem to the new state and queues an `:execute_phase` internal event 4. The internal event triggers the module’s `__phase_handler__/3` for the target phase 5. Phase handlers can transition again, emit events, update state, or stop the agent 6. `stop()` terminates the agent process normally 7. `emit(event)` appends to the event log, queryable via `get_events/1` Each compiled Skein agent generates: * `start_link/1` — start the agent with initial params * `__phases__/0` — return phase metadata (variants and transitions) * `__start_handler__/2` — the `on start(...)` handler * `__phase_handler__/3` — phase-specific handlers dispatched by phase name See the [Agents page](/Skein/runtime/agents/) for detailed documentation. ### `Skein.Runtime.Http` [Section titled “Skein.Runtime.Http”](#skeinruntimehttp) The HTTP client wraps Erlang’s built-in `:httpc` module. It is the target of compiled `http.get`, `http.post`, etc. calls. **API:** ```elixir Skein.Runtime.Http.get(url, capabilities) #=> {:ok, body} | {:error, reason} Skein.Runtime.Http.post(url, body, capabilities) #=> {:ok, body} | {:error, reason} Skein.Runtime.Http.put(url, body, capabilities) Skein.Runtime.Http.patch(url, body, capabilities) Skein.Runtime.Http.delete(url, capabilities) ``` Every call: 1. Checks the URL against `capabilities` via `Skein.Runtime.Capability` 2. If allowed, makes the HTTP request 3. Records a trace span with timing and outcome 4. Returns `{:ok, body}` for 2xx responses, `{:error, reason}` otherwise The runtime uses `:httpc` rather than a third-party HTTP library to minimize dependencies. The `:inets` and `:ssl` OTP applications are started automatically. ### `Skein.Runtime.Capability` [Section titled “Skein.Runtime.Capability”](#skeinruntimecapability) Provides runtime capability enforcement — the second layer of defense beyond compile-time checking. **API:** ```elixir Skein.Runtime.Capability.check_http(url, capabilities) #=> :ok | {:error, reason} Skein.Runtime.Capability.extract_host(url) #=> {:ok, host} | {:error, reason} ``` **How checking works:** 1. Extract the host from the URL 2. Filter capabilities to find `http.out` entries 3. If any capability has empty params (wildcard), allow 4. Otherwise, check if the URL host appears in any capability’s params 5. Return `:ok` or `{:error, "Host 'x' not declared in http.out capabilities"}` ### `Skein.Runtime.Handler` [Section titled “Skein.Runtime.Handler”](#skeinruntimehandler) Dispatches incoming HTTP requests to compiled Skein handler functions. Handles route matching with path parameters, request construction, and response encoding. **API:** ```elixir Skein.Runtime.Handler.dispatch(module, method, path, headers, body) #=> {:ok, status, body, content_type} | {:error, reason} # content_type is :json, :text, or :html ``` **Features:** * Route matching with path parameters (`:id` syntax) * HTTP method dispatching (GET, POST, PUT, PATCH, DELETE) * Request map construction with `params`, `headers`, `body`, `method`, `path` * JSON response encoding via Jason * Trace span recording for each dispatched request ### `Skein.Runtime.Queue` [Section titled “Skein.Runtime.Queue”](#skeinruntimequeue) In-memory message queue dispatch for compiled Skein queue handlers. Manages subscriptions between queue names and handler functions, dispatching published messages asynchronously. **API:** ```elixir Skein.Runtime.Queue.subscribe("order-events", MyModule, :__handler_0__) #=> :ok Skein.Runtime.Queue.publish("order-events", %{body: "payload"}) #=> :ok (dispatches asynchronously) Skein.Runtime.Queue.list_queues() #=> ["order-events", "priority-events"] ``` Features: * In-order message delivery within a single queue * Multiple subscribers per queue * Trace span recording for each dispatched message * For testing, `subscribe_fn/2` accepts a plain function ### `Skein.Runtime.Schedule` [Section titled “Skein.Runtime.Schedule”](#skeinruntimeschedule) Cron-based schedule dispatch for compiled Skein schedule handlers. Registered handlers **fire automatically**: a periodic tick (1s by default) evaluates each cron expression against the wall clock and fires matching handlers at most once per cron minute. `Skein.Runtime.Server` registers every `:schedule` entry from a module’s `__handlers__/0` at startup, so `skein run` services fire on schedule with no manual intervention. **API:** ```elixir Skein.Runtime.Schedule.register("*/5 * * * *", MyModule, :__handler_0__) #=> :ok Skein.Runtime.Schedule.trigger("*/5 * * * *") #=> :ok (triggers all handlers for this expression) Skein.Runtime.Schedule.parse_cron("0 * * * *") #=> {:ok, %{minute: "0", hour: "*", day: "*", month: "*", weekday: "*"}} Skein.Runtime.Schedule.list_schedules() #=> ["*/5 * * * *", "0 0 * * *"] ``` Features: * Automatic firing on a configurable tick (`schedule_tick_ms`, default 1s; `schedule_auto_tick: false` disables — the test env does) * Full 5-field cron matching: `*`, `n`, `a-b`, `*/n`, `a-b/n`, comma lists; weekday 0/7 = Sunday; restricted day-of-month and weekday combine with OR (standard cron rule) * Per-minute dedup — no duplicate firings within the same cron minute * Invalid cron expressions are rejected at registration (`{:error, reason}`) * Manual triggering (`trigger/1`) and deterministic clock injection (`tick_at/1`) for testing * Trace span recording for each fired handler ### `Skein.Runtime.Store` [Section titled “Skein.Runtime.Store”](#skeinruntimestore) ETS-backed key-value storage that compiled `store.table` effect calls target — the only storage path compiled programs hit today (there is no backend-selection mechanism). Store tables are typed (C5, #255): the capability names the table’s record type, the compiler type-checks every operation against it, and writes are schema-checked at runtime. **API:** ```elixir Skein.Runtime.Store.get(table, id, capabilities) #=> {:ok, record} | {:error, :not_found} Skein.Runtime.Store.put(table, record, capabilities) #=> {:ok, record} Skein.Runtime.Store.delete(table, id, capabilities) #=> {:ok, id} # Filters are an equality map; the matching records come back wrapped in :ok Skein.Runtime.Store.query(table, %{status: "active"}, capabilities) #=> {:ok, [record, ...]} ``` Every operation: 1. Validates `store.table` capability for the target table 2. Performs the ETS operation 3. Records a trace span with timing and outcome 4. Returns `{:ok, _}` / `{:error, _}` — including `query`, whose `:ok` payload is the matching-record list ### `Skein.Runtime.StoreEcto` [Section titled “Skein.Runtime.StoreEcto”](#skeinruntimestoreecto) Ecto-backed storage backend that performs real database operations against SQLite3 (the `ecto_sqlite3` adapter is hardcoded in `Skein.Runtime.Repo`). Uses dynamically-generated Ecto schema modules mapped to Skein type declarations. **API:** Same as `Skein.Runtime.Store` (get, put, delete, query) with identical capability enforcement and tracing. **Supporting modules:** * `Skein.Runtime.EctoSchema` — generates Ecto schema modules from Skein type fields and annotations (`@primary`, `@unique`) * `Skein.Runtime.MigrationGen` — generates and executes Ecto migrations to create/modify database tables * `Skein.Runtime.Repo` — Ecto Repo configured for SQLite3 via `ecto_sqlite3` ```elixir # Generate schema and migration from Skein type info {:ok, schema_mod} = EctoSchema.build_schema("users", user_fields) {:ok, migration_mod} = MigrationGen.build_migration("users", user_fields) :ok = MigrationGen.run_migration(Skein.Runtime.Repo, migration_mod) # Register and use StoreEcto.register_schema("users", schema_mod) StoreEcto.put("users", %{id: "u1", email: "alice@test.com", name: "Alice"}, caps) ``` ### `Skein.Runtime.Memory` [Section titled “Skein.Runtime.Memory”](#skeinruntimememory) Scoped key-value memory for Skein agents and modules. All namespaces share a single ETS table (`:skein_memory`) keyed by `{namespace, key}` — namespaces are never separate tables. Called by compiled `memory.put`, `memory.get`, `memory.delete`, and `memory.list` effect calls. **API:** ```elixir Skein.Runtime.Memory.put("sessions", "key1", %{user: "alice"}, capabilities) #=> {:ok, %{user: "alice"}} Skein.Runtime.Memory.get("sessions", "key1", capabilities) #=> {:ok, %{user: "alice"}} Skein.Runtime.Memory.delete("sessions", "key1", capabilities) #=> {:ok, "key1"} Skein.Runtime.Memory.list("sessions", "user:", capabilities) #=> ["user:alice", "user:bob"] ``` Every operation: 1. Validates `memory.kv` capability for the target namespace 2. Performs the ETS operation in the shared `:skein_memory` table, keyed by `{namespace, key}` (a single static table — namespace strings are never converted to atoms) 3. Records a trace span with timing and outcome via the unified EventStore 4. Each mutation (put/delete) also emits a `:state_change` event for audit and replay 5. Returns `{:ok, _}` or `{:error, _}` Memory state can be reconstructed from the event stream: ```elixir # Rebuild state from :state_change events (event-sourced) Skein.Runtime.Memory.rebuild_from_events("sessions") #=> %{"user_id" => "u-123", "session" => "s-456"} ``` ### `Skein.Runtime.Llm` [Section titled “Skein.Runtime.Llm”](#skeinruntimellm) Provider-agnostic LLM client. Provides unstructured chat, schema-constrained JSON, and streaming endpoints. Uses a pluggable backend system for testing and production. **API:** ```elixir Skein.Runtime.Llm.chat("claude-opus-4-8", "You are helpful", "What is 2+2?", capabilities) #=> {:ok, "4"} Skein.Runtime.Llm.json("claude-opus-4-8", "Evaluate refund", input, schema, capabilities) #=> {:ok, %{"action" => "approve", "amount" => 100}} Skein.Runtime.Llm.stream("claude-opus-4-8", "Be helpful", "Hello", on_chunk_fn, capabilities) #=> {:ok, "Hello, world!"} (chunks delivered to on_chunk_fn as they arrive) Skein.Runtime.Llm.embed("voyage-3-large", "some text", capabilities) #=> {:ok, [0.013, -0.027, ...]} (vector dimensionality depends on the model) ``` Every operation: 1. Validates `model` capability for the requested model 2. Dispatches to the active backend (defaults to `TestBackend` for deterministic testing) 3. For `json/5`, parses the response as JSON and validates structure 4. For `stream/5`, delivers each chunk to the callback, then returns the assembled text 5. Records a trace span with model, timing, and outcome 6. Returns `{:ok, _}` or `{:error, }` — the frozen matchable form (C2/#297), e.g. `{:provider_error, code, message}`; internal `%Llm.Error{}` structs are converted at the public boundary **Production backends** (selected via the `[llm]` profile in skein.toml): * `Skein.Runtime.Llm.AnthropicBackend` — Anthropic Messages API (the production default) * `Skein.Runtime.Llm.OpenAiCompatibleBackend` — any OpenAI-compatible `/chat/completions` server (local model servers, Voyage AI embeddings) * `Skein.Runtime.Llm.BedrockBackend` — Amazon Bedrock Converse API (SigV4-signed, with `model_map` remapping to Bedrock model IDs) **Test backends:** * `Skein.Runtime.Llm.TestBackend` — deterministic responses for testing * `Skein.Runtime.Llm.StreamingTestBackend` — deterministic streaming chunks for testing * `Skein.Runtime.Llm.FailingBackend` — always returns errors (for error-path testing) * `Skein.Runtime.Llm.FailingStreamBackend` — always returns errors during streaming * `Skein.Runtime.Llm.InvalidJsonBackend` — returns invalid JSON (for parse-error testing) * `Skein.Runtime.Llm.DynamicStreamBackend` — configurable chunks via `{module, chunks}` tuple Custom backends implement the `Skein.Runtime.Llm.Backend` behaviour. **Error ABI (`LlmError`, C2/#297)** — what compiled Skein sees, matchable as `Err(LlmError.(…))`: | ABI form | Skein variant | Description | | -------------------------------------------------- | ----------------- | -------------------------------------------- | | `{:parse_failed, raw, expected_type, parse_error}` | `ParseFailed` | Response couldn’t be parsed as expected type | | `{:refused, reason}` | `Refused` | LLM refused to generate a response | | `{:rate_limit, retry_after_ms}` | `RateLimit` | Rate limited (retry-after in ms) | | `{:timeout, elapsed_ms}` | `Timeout` | Request timed out | | `{:content_filtered, filter}` | `ContentFiltered` | Response was filtered by content policy | | `{:invalid_schema, violations}` | `InvalidSchema` | Response didn’t match expected JSON schema | | `{:provider_error, code, message}` | `ProviderError` | Provider returned an error | | `{:denied, reason}` | `Denied` | Capability/scope denial | Internally the backends use `Skein.Runtime.Llm.Error` structs; `Llm.Error.to_abi/1` converts at the public boundary. ### `Skein.Runtime.Server` [Section titled “Skein.Runtime.Server”](#skeinruntimeserver) A production-grade HTTP server powered by Bandit + Plug that serves compiled Skein handlers. **API:** ```elixir Skein.Runtime.Server.start_link(module: MyModule, port: 4000) #=> {:ok, pid} ``` **Features:** * Bandit HTTP server with concurrent request handling * Plug-based router built dynamically from compiled handler metadata (`Skein.Runtime.Router`) * Routes requests to handler functions via `Skein.Runtime.Handler` * Serves trace data at `GET /__skein/traces` * JSON response encoding with proper content-type headers * Exception handling with 500 responses for handler errors ### `Skein.Runtime.Request` [Section titled “Skein.Runtime.Request”](#skeinruntimerequest) Provides request body parsing and validation for `req.json[T]` expressions in handlers. **API:** ```elixir Skein.Runtime.Request.json(req_map, json_schema) #=> {:ok, parsed_map} | {:error, reason} ``` **Features:** * Parses JSON request body from the handler’s `req` map * Validates against compile-time JSON Schema derived from Skein type declarations * Checks required fields and field types (string, integer, number, boolean, array, object) * Returns structured error messages for validation failures ### `Skein.Runtime.EventStore` [Section titled “Skein.Runtime.EventStore”](#skeinruntimeeventstore) Unified append-only event log for the entire runtime. All trace spans, user events (`event.log`), memory state changes, and annotations flow through a single ETS ordered set (`:skein_events`). The in-memory log is size-bounded: once it grows past the configured maximum (`config :skein_runtime, :event_store_max_events`, default 100,000), the oldest events are evicted on append. Durable persistence is **opt-in** ([#299](https://github.com/kormie/Skein/issues/299)): `Skein.Runtime.EventStore.Persistence.enable(db_path)` — which `skein run` calls by default, writing to `/.skein/events.db` (`skein run --no-persist` opts out) — makes every ordinary append also write the event asynchronously to SQLite and reloads previously persisted events into the ETS log on startup, so a restarted service sees its history. ETS eviction never deletes persisted rows: SQLite keeps the full history beyond the in-memory bound. Persisted events round-trip through JSON, so a reloaded event is not bit-identical to the original (unknown keys and non-`kind`-like atom values come back as strings); the exact reloaded shape is pinned in the `Persistence` moduledoc and FROZEN as of the Wave F gate (#332, frozen vectors in `event_store_freeze_test.exs`) — shapes only gain fields within a major. Without `enable/1` the log is in-memory only and nothing survives a restart. **API:** ```elixir # Append any event Skein.Runtime.EventStore.append(%{kind: :http, method: :get, url: "/api"}) #=> :ok (auto-assigns id, timestamp, _key) # Log a user event (compiled event.log() calls target this). # The first argument is the stream label, threaded in by the compiler # from the module's scoped capability event.log(stream) declaration # (nil when the declaration is parameterless). Skein.Runtime.EventStore.log(nil, "user.login", %{user: "alice"}, capabilities) #=> {:ok, "user.login"} (Skein contract: Result[String, String] — a # scope-label denial is {:error, reason}) # Query by kind or any field Skein.Runtime.EventStore.query(kind: :user_event) Skein.Runtime.EventStore.query(kind: :state_change, namespace: "sessions") # Recent events (newest first) Skein.Runtime.EventStore.recent(10) # Count events Skein.Runtime.EventStore.count() Skein.Runtime.EventStore.count(kind: :effect) # Chronological snapshot for golden tests Skein.Runtime.EventStore.snapshot() #=> [oldest_event, ..., newest_event] # Clear all events Skein.Runtime.EventStore.clear() ``` **Event kinds:** | Kind | Source | Description | | ------------------------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------ | | `:http`, `:memory`, `:llm`, `:store`, `:tool`, `:process`, `:timer` | Effect calls | Effect spans with timing, outcome, method | | `:annotation` | `trace.annotate(key, value)` | Key-value markers | | `:user_event` | `event.log(name, data)`, agent `emit` | User-defined structured events; agent emits carry `agent`/`instance_id`/`phase` tags | | `:state_change` | `memory.put` / `memory.delete` | Memory mutation audit trail with key/value data | Every event carries: `id` (unique hex), `timestamp` (monotonic microseconds), `kind`, and kind-specific fields. ### `Skein.Runtime.Trace` [Section titled “Skein.Runtime.Trace”](#skeinruntimetrace) Timing and instrumentation facade over `EventStore`. All effect wrappers (HTTP, Memory, LLM, etc.) call `Trace.with_span` to record spans with automatic timing. **API:** ```elixir # Execute a function with automatic tracing Skein.Runtime.Trace.with_span(%{kind: :http, method: :get, url: url}, fn -> {:ok, result} end) # Add a trace annotation Skein.Runtime.Trace.annotate("step", "analysis_complete") # Query recent events (delegates to EventStore.recent) Skein.Runtime.Trace.recent_spans(10) # Record a span manually (delegates to EventStore.append) Skein.Runtime.Trace.record_span(%{kind: :http, method: :get, url: "/test"}) ``` `with_span/2` handles both normal returns and exceptions: * `{:ok, _}` -> outcome `:ok` * `{:error, _}` -> outcome `:error` * Exception -> outcome `:error` with message, then re-raises ### Other Runtime Modules [Section titled “Other Runtime Modules”](#other-runtime-modules) | Module | Purpose | | -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Skein.Runtime.Topic` | In-memory pub/sub topic dispatch — published messages fan out to **all** subscribers of a topic | | `Skein.Runtime.Timer` | One-shot (`after`) and recurring (`interval`) timers with cancellation, tracked in ETS | | `Skein.Runtime.Process` | `process.spawn` — short-lived tasks under a DynamicSupervisor with crash isolation and tracing | | `Skein.Runtime.Tool` | Tool registry and dispatch — `call/3` executes registered tools with schema-checked input/output | | `Skein.Runtime.Idempotent` | `idempotent(key)` guard — tracks processed keys in ETS so handlers skip duplicates | | `Skein.Runtime.Replay` | Replay engine for golden trace tests — deterministic playback of recorded event streams | | `Skein.Runtime.SupervisorHost` | Boots real OTP supervisors from compiled `supervisor` declarations ([#325](https://github.com/kormie/Skein/issues/325)) — restart policies, `max_restarts` intensity, `:child_started` events | | `Skein.Runtime.JsonSchema` | The one recursive schema validator/decoder (C3) shared by `req.json[T]`, `llm.json[T]`, and tool input/output | | `Skein.Runtime.EventStore.Persistence` | Opt-in SQLite write-through for the event log (C6) — enabled by default under `skein run` | ## How Compiled Code Uses the Runtime [Section titled “How Compiled Code Uses the Runtime”](#how-compiled-code-uses-the-runtime) The Skein code generator transforms effect calls into runtime calls: ```skein -- Skein source: http.get(url) -- Compiles to (Core Erlang): call 'Elixir.Skein.Runtime.Http':'get'(Url, Capabilities) ``` ```skein -- Skein source: store.users.get(id) -- Compiles to (Core Erlang): call 'Elixir.Skein.Runtime.Store':'get'("users", Id, Capabilities) ``` ```skein -- Skein source (in a module declaring `capability memory.kv("sessions")`): memory.put(key, value) -- Compiles to (Core Erlang) — the namespace comes from the capability -- declaration, not the call site: call 'Elixir.Skein.Runtime.Memory':'put'("sessions", Key, Value, Capabilities) ``` ```skein -- Skein source: llm.chat("claude-opus-4-8", "system prompt", input) -- Compiles to (Core Erlang): call 'Elixir.Skein.Runtime.Llm':'chat'("claude-opus-4-8", "system prompt", Input, Capabilities) ``` Where `Capabilities` is a literal list built from the module’s `capability` declarations at compile time. ## Dependencies [Section titled “Dependencies”](#dependencies) The runtime manages its external dependencies carefully: | Dependency | Purpose | | -------------- | ----------------------------- | | `:inets` (OTP) | HTTP client (`:httpc`) | | `:ssl` (OTP) | HTTPS support | | `jason` | JSON encoding/decoding | | `bandit` | HTTP server | | `plug` | Web framework | | `ecto` | Database abstraction | | `ecto_sql` | SQL adapter framework | | `ecto_sqlite3` | SQLite3 adapter for local dev | ## Test Coverage [Section titled “Test Coverage”](#test-coverage) The runtime has comprehensive test coverage: * **Property tests** (StreamData) covering capability host matching, wildcard behavior, URL extraction, store operations (ETS and Ecto), request validation, tool operations, LLM streaming, memory KV operations, queue dispatch, schedule cron parsing, and trace recording * **Stateful property tests** (PropCheck) modeling memory, queue, schedule, and agent lifecycle as abstract state machines with random command sequences * **Unit tests** covering agents, HTTP client, capability enforcement, handler dispatch, router, store operations (ETS and Ecto), Ecto schema generation, migration generation, memory, LLM client, Bandit HTTP server, queue dispatch, schedule dispatch, trace recording, and replay engine # Storage > How Skein's typed, schema-checked store.table operations execute against the ETS-backed runtime store, and the status of the unwired Ecto/SQLite path. ## Overview [Section titled “Overview”](#overview) Skein provides storage through the `store.table` capability. Store tables are typed: every declaration names both the table and the record type it stores — `capability store.table("users", User)` — where the record type is a declared `type` in the same module with exactly one `@primary` field (the get/delete key). The analyzer type-checks every `store.
` operation against that record type, and writes are schema-checked at runtime. At runtime, every compiled `store.*` call executes against `Skein.Runtime.Store` — an ETS-backed key-value store (a single `:skein_store` table keyed by string table names) with capability enforcement and tracing. That is the only storage path compiled programs hit today. Note the storage is **in-memory**: store tables do not survive a restart. Typed store tables (C5, [#255](https://github.com/kormie/Skein/issues/255)) shipped as schema-checked ETS — durable persistence for store tables was deliberately **not** part of C5 and is not planned as such. (The [EventStore](/Skein/runtime/overview/) has opt-in SQLite persistence, but that is a different subsystem: it persists the event log, not store tables.) An Ecto/SQLite persistent-table layer also exists in the codebase (`StoreEcto`, `EctoSchema`, `MigrationGen`, `Repo`), but it is **not wired into compilation or boot** — nothing registers schemas or runs migrations for compiled programs, and there is no backend-selection mechanism. It is library code exercised only by its own tests, and the C5 typed-table work deliberately did not revive it. ## Architecture [Section titled “Architecture”](#architecture) The live store module: | Module | Purpose | | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | `Skein.Runtime.Store` | ETS-backed CRUD operations (get, put, delete, query) with capability enforcement and tracing — the target of every compiled `store.*` call | The unwired Ecto layer (dead library code today — typed tables (C5) landed on the ETS store instead, and this path was deliberately not revived): | Module | Purpose | | ---------------------------- | --------------------------------------------------------------------------------- | | `Skein.Runtime.StoreEcto` | CRUD operations (get, put, delete, query) with capability enforcement and tracing | | `Skein.Runtime.EctoSchema` | Dynamically generates Ecto schema modules from Skein type declarations | | `Skein.Runtime.MigrationGen` | Generates and executes Ecto migrations to create/modify tables | | `Skein.Runtime.Repo` | Ecto Repo configured for SQLite3 via `ecto_sqlite3` | ## The Unwired Ecto Path [Section titled “The Unwired Ecto Path”](#the-unwired-ecto-path) The sections below describe what the Ecto modules do when called directly (as their tests do). Compiled Skein programs do **not** invoke any of this today. ### 1. Schema Generation [Section titled “1. Schema Generation”](#1-schema-generation) Given a table name and field descriptions (the shape the compiler could extract from a `type` declaration), `EctoSchema.build_schema/2` creates a dynamic Ecto schema module. ```elixir fields = [ %{name: "id", type: "Uuid", annotations: [%{name: "primary"}]}, %{name: "email", type: "String", annotations: [%{name: "unique"}]}, %{name: "name", type: "String", annotations: []} ] {:ok, schema_mod} = Skein.Runtime.EctoSchema.build_schema("users", fields) ``` **Type mapping:** | Skein Type | Ecto Type | | ----------- | --------------------- | | `String` | `:string` | | `Int` | `:integer` | | `Float` | `:float` | | `Bool` | `:boolean` | | `Uuid` | `:binary_id` | | `Instant` | `:utc_datetime` | | `Option[T]` | Inner type (nullable) | ### 2. Migration Generation [Section titled “2. Migration Generation”](#2-migration-generation) `MigrationGen.build_migration/2` creates an Ecto migration module that: * Creates the table with the correct column types * Sets the primary key based on `@primary` annotations * Adds unique indexes for `@unique` annotations ```elixir {:ok, migration_mod} = Skein.Runtime.MigrationGen.build_migration("users", fields) :ok = Skein.Runtime.MigrationGen.run_migration(Skein.Runtime.Repo, migration_mod) ``` ### 3. CRUD Operations [Section titled “3. CRUD Operations”](#3-crud-operations) `StoreEcto` mirrors the `store.*` API, but requires manual schema registration — nothing does this automatically for compiled programs: ```elixir # Register the schema (manual — only the StoreEcto tests do this today) StoreEcto.register_schema("users", schema_mod) # Insert or upsert a record StoreEcto.put("users", %{id: "u1", email: "alice@test.com", name: "Alice"}, caps) #=> {:ok, %{id: "u1", email: "alice@test.com", name: "Alice"}} # Get by primary key StoreEcto.get("users", "u1", caps) #=> {:ok, %{id: "u1", email: "alice@test.com", name: "Alice"}} # Query with filters # Filters are an equality map; matching records come back wrapped in :ok StoreEcto.query("users", %{email: "alice@test.com"}, caps) #=> {:ok, [%{id: "u1", email: "alice@test.com", name: "Alice"}]} # Delete by primary key StoreEcto.delete("users", "u1", caps) #=> {:ok, "u1"} ``` Every operation: 1. Validates `store.table` capability for the target table 2. Performs the Ecto query (using upsert semantics with `ON CONFLICT DO UPDATE` for `put`) 3. Records a trace span with timing and outcome 4. Returns `{:ok, result}` or `{:error, reason}` ## Skein Source Example [Section titled “Skein Source Example”](#skein-source-example) ```skein module UserService { capability store.table("users", User) capability http.in type User { id: Uuid @primary email: String @unique name: String created_at: Instant } handler http GET "/users/:id" (req) -> { match store.users.get(req.params.id) { Ok(u) -> respond.json(200, u) Err(NotFound) -> respond.json(404, { error: "not found" }) } } handler http POST "/users" (req) -> { let data = req.json[User]()? let user = store.users.put(data)! respond.json(201, user) } } ``` ## Compiled Code [Section titled “Compiled Code”](#compiled-code) The code generator transforms `store.*` calls into runtime calls: ```skein -- Skein source: store.users.get(id) -- Compiles to (Core Erlang): call 'Elixir.Skein.Runtime.Store':'get'("users", Id, Capabilities) ``` Where `Capabilities` is a literal list built from the module’s `capability` declarations at compile time. ## The ETS Store [Section titled “The ETS Store”](#the-ets-store) `Skein.Runtime.Store` is the store: an in-memory, ETS-backed key-value implementation that every compiled `store.*` call targets. It requires no database setup. There is no backend-selection or dispatch mechanism — compiled code calls `Skein.Runtime.Store` directly, so data does not survive a restart. Typed, schema-checked tables landed with C5 ([#255](https://github.com/kormie/Skein/issues/255)), but on this ETS store — there is no durable persistence for store tables. Only the EventStore (a separate subsystem for the event log) offers SQLite persistence. ## Configuration [Section titled “Configuration”](#configuration) The (unwired) Ecto layer uses SQLite3 — the `ecto_sqlite3` adapter is hardcoded in `Skein.Runtime.Repo` at compile time, and `postgrex` is not a dependency. If you start the Repo yourself, connection options (database path, pool size) come from standard Ecto config: ```elixir # Skein.Runtime.Repo configuration config :skein_runtime, Skein.Runtime.Repo, database: "skein_dev.db", pool_size: 5 ``` Other database adapters (e.g. Postgres) are a possible future direction, but nothing currently builds on this layer — typed store tables (C5) run on the ETS store. # Property-Based Tests > How Skein uses StreamData generators for property-based testing. ## Why Property Tests? [Section titled “Why Property Tests?”](#why-property-tests) Unit tests verify specific examples. Property tests verify **invariants across large input spaces** using randomly generated data. For a compiler, this means: * Testing the lexer against thousands of random valid identifiers * Testing the parser against randomly generated Skein programs * Testing the code generator by compiling random programs and verifying runtime behavior matches expectations ## Libraries [Section titled “Libraries”](#libraries) | Library | Purpose | Status | | --------------------------------- | -------------------------------- | ------------------------------------------- | | `StreamData` / `ExUnitProperties` | Generator-based property testing | Active (see CI for current property counts) | | `PropCheck` (PropEr) | Stateful/state-machine testing | Active | `StreamData` is used for stateless property tests (data-in/data-out). `PropCheck` is used for stateful testing where a model tracks expected system state across command sequences. ## Test Files [Section titled “Test Files”](#test-files) ```plaintext apps/skein_compiler/test/skein/ lexer_property_test.exs # StreamData parser_property_test.exs # StreamData analyzer_property_test.exs # StreamData codegen_soundness_property_test.exs # StreamData (B4) codegen/ core_erlang_property_test.exs # StreamData schema_gen_property_test.exs # StreamData apps/skein_runtime/test/skein/runtime/ capability_property_test.exs # StreamData event_store_property_test.exs # StreamData handler_property_test.exs # StreamData idempotent_property_test.exs # StreamData llm_embed_property_test.exs # StreamData llm_stream_property_test.exs # StreamData memory_property_test.exs # StreamData process_property_test.exs # StreamData queue_property_test.exs # StreamData request_property_test.exs # StreamData schedule_property_test.exs # StreamData store_property_test.exs # StreamData store_ecto_property_test.exs # StreamData timer_property_test.exs # StreamData tool_property_test.exs # StreamData topic_property_test.exs # StreamData trace_property_test.exs # StreamData agent_statem_test.exs # PropCheck stateful memory_statem_test.exs # PropCheck stateful queue_statem_test.exs # PropCheck stateful schedule_statem_test.exs # PropCheck stateful topic_statem_test.exs # PropCheck stateful ``` ## Lexer Properties [Section titled “Lexer Properties”](#lexer-properties) The lexer property tests use generators for valid identifiers, integers, and strings, then verify tokenization invariants. ### Generators [Section titled “Generators”](#generators) ```elixir # Lowercase identifiers: starts with a-z, followed by a-z, 0-9, _ defp lower_ident_gen do gen all first <- StreamData.member_of(Enum.to_list(?a..?z)), rest <- StreamData.list_of( StreamData.member_of(Enum.to_list(?a..?z) ++ Enum.to_list(?0..?9) ++ [?_]), min_length: 0, max_length: 20 ) do name = List.to_string([first | rest]) if name in @keywords, do: "z" <> name, else: name end end ``` Note the keyword avoidance — if a random identifier happens to match a keyword, it’s prefixed with `"z"`. ### Properties Verified [Section titled “Properties Verified”](#properties-verified) * Any valid lowercase identifier tokenizes to `:ident` * Any valid uppercase identifier tokenizes to `:upper_ident` * Any positive integer tokenizes to `:int` with that value * Quoted strings produce `:string` tokens * Simple string content round-trips (literal is preserved) * Token lists always end with `:eof` * Token positions are always positive (line >= 1, col >= 1) * All keywords tokenize to their corresponding atoms * Whitespace-separated identifiers produce correct token count * Newlines increment line numbers correctly * String interpolation preserves the identifier name ## Parser Properties [Section titled “Parser Properties”](#parser-properties) The parser properties generate **valid Skein source programs** and verify structural invariants. This includes functions, tool declarations, scenario tests, and golden tests. ### Source Generators [Section titled “Source Generators”](#source-generators) The generators build valid Skein source strings compositionally: ```elixir # Generate a valid function defp fn_gen do gen all name <- lower_ident_gen(), params <- StreamData.list_of(param_gen(), min_length: 0, max_length: 3), ret_type <- type_name_gen(), body <- body_expr_gen() do params_str = Enum.join(params, ", ") "fn #{name}(#{params_str}) -> #{ret_type} {\n #{body}\n }" end end # Generate a valid module defp module_gen do gen all mod_name <- upper_ident_gen(), fns <- StreamData.list_of(fn_gen(), min_length: 1, max_length: 4) do body = Enum.map_join(fns, "\n ", & &1) "module #{mod_name} {\n #{body}\n}" end end ``` ### Properties Verified [Section titled “Properties Verified”](#properties-verified-1) * Any generated module source lexes and parses successfully * Parsed module name matches the generated name * Number of parsed fn declarations matches number generated * Every parsed function has a return type (`%AST.TypeRef{}`) * Every parsed function has a block body (`%AST.Block{}`) * Every AST node carries source location metadata (line >= 1, col >= 1) * Match on booleans always produces exactly 2 arms * Empty modules parse with zero declarations * Any generated scenario lexes and parses successfully * Scenario description matches generated description * Scenario given var count matches generated bindings * Scenario preserves source location metadata * Any generated golden declaration lexes and parses successfully * Golden trace file matches generated path * Golden preserves source location metadata ## CodeGen Properties [Section titled “CodeGen Properties”](#codegen-properties) The codegen properties test the **full pipeline** — generate random inputs, compile Skein source to BEAM, call the compiled functions, and verify results match Elixir semantics. ### Unique Module Names [Section titled “Unique Module Names”](#unique-module-names) Each property test iteration creates a unique module to avoid BEAM module name collisions: ```elixir defp unique_module_name do counter = System.unique_integer([:positive, :monotonic]) "PropMod#{counter}" end ``` ### Example Property [Section titled “Example Property”](#example-property) ```elixir property "integer addition compiles and computes correctly" do check all a <- StreamData.integer(-1000..1000), b <- StreamData.integer(-1000..1000) do mod_name = unique_module_name() source = """ module #{mod_name} { fn add(a: Int, b: Int) -> Int { a + b } } """ {:module, mod} = Compiler.compile_string(source) assert mod.add(a, b) == a + b end end ``` This generates random integer pairs, compiles a Skein `add` function for each, and verifies the result matches Elixir’s `+` operator. ### Properties Verified [Section titled “Properties Verified”](#properties-verified-2) * Integer addition computes correctly * Integer subtraction computes correctly * Integer multiplication computes correctly * Comparison `>` produces correct booleans * Equality `==` produces correct booleans * String interpolation round-trips alphanumeric input * Let bindings preserve computed values * Match on `> 0` correctly classifies positive vs non-positive * Plain string literals return exact strings ## PropCheck Stateful Tests [Section titled “PropCheck Stateful Tests”](#propcheck-stateful-tests) PropCheck stateful tests use the `:proper_statem` behaviour to model a system as an abstract state machine. PropEr generates random **command sequences**, runs them against the real system, and checks that postconditions hold after each command. On failure, it shrinks to the minimal reproducing sequence. ### Architecture [Section titled “Architecture”](#architecture) Each stateful test defines: | Callback | Purpose | | ----------------- | -------------------------------------------------- | | `initial_state/0` | Starting model state | | `command/1` | Generate next command based on current model state | | `precondition/2` | Can this command run in this state? | | `postcondition/3` | Does the real result match the model? | | `next_state/3` | Update model after command execution | ### Memory State Machine [Section titled “Memory State Machine”](#memory-state-machine) Models the ETS-backed memory store as a plain map. Verifies put/get/delete/list operations are consistent: ```elixir # Model: %{key => value} def initial_state, do: %{} def postcondition(state, {:call, _, :do_get, [key]}, result) do case Map.fetch(state, key) do {:ok, expected} -> result == {:ok, expected} :error -> result == {:error, :not_found} end end def next_state(state, _result, {:call, _, :do_put, [key, value]}) do Map.put(state, key, value) end ``` ### Queue State Machine [Section titled “Queue State Machine”](#queue-state-machine) Models queue subscriptions as `%{queue_name => subscriber_count}`. Verifies subscribe/publish/list/reset maintain consistency. ### Schedule State Machine [Section titled “Schedule State Machine”](#schedule-state-machine) Models schedule registrations as `%{cron_expr => handler_count}`. Verifies register/trigger/list/reset maintain consistency. ### Topic State Machine [Section titled “Topic State Machine”](#topic-state-machine) Models pub/sub topics as `%{topic_name => subscriber_count}`. Verifies subscribe/publish/list/reset maintain consistency. ### Agent Lifecycle State Machine [Section titled “Agent Lifecycle State Machine”](#agent-lifecycle-state-machine) Tests the agent query API against compiled agents with known phase behavior: * **ParkingAgent** — parks in `:waiting` phase (allows queries) * **TwoPhaseAgent** — transitions `Init -> Active`, parks in `:active` * **StoppingAgent** — transitions to `:done` and stops immediately The model tracks `{pid, expected_phase, alive?}` tuples and verifies `get_phase/1`, `get_state/1`, and `get_events/1` return consistent results. ## Writing New Properties [Section titled “Writing New Properties”](#writing-new-properties) ### Pattern [Section titled “Pattern”](#pattern) ```elixir property "description of the invariant" do check all input <- generator() do # Exercise the code under test result = function_under_test(input) # Assert the invariant holds assert invariant(result, input) end end ``` ### Generator Tips [Section titled “Generator Tips”](#generator-tips) * Use `StreamData.member_of/1` for selecting from a known set * Use `StreamData.one_of/1` for choosing between generator types * Use `gen all` for composing multiple generators * Guard against accidentally generating keywords when making identifiers * Use `System.unique_integer([:positive, :monotonic])` for unique names in codegen tests ### Running Property Tests [Section titled “Running Property Tests”](#running-property-tests) ```bash # All tests including properties mix test # Just the StreamData property tests (the `property` macro tags these; # the PropCheck stateful suites are plain tests and are NOT selected here) mix test --only property # With more iterations (default is 100) # Configure in the check call: check all ..., max_runs: 1000 do ``` # Unit Tests > How the Skein compiler is tested with ExUnit. ## Testing Philosophy [Section titled “Testing Philosophy”](#testing-philosophy) **TDD is mandatory.** Tests are written before or alongside implementation — never after. Every public function must have tests covering its happy path and error cases. ## Test Organization [Section titled “Test Organization”](#test-organization) ```plaintext apps/skein_compiler/test/ skein/ lexer_test.exs # lexer unit tests parser_test.exs # parser unit tests (incl. scenario/golden) analyzer_test.exs # analyzer unit tests codegen/ core_erlang_test.exs # codegen integration tests schema_gen_test.exs # JSON Schema generation tests integration/ memory_llm_test.exs # memory + llm pipeline tests tool_test.exs # tool declaration tests test_construct_test.exs # test/scenario/golden constructs req_json_test.exs # request JSON handling apps/skein_runtime/test/ skein/runtime/ # runtime behaviour tests (agents, store, queue, replay, ...) apps/skein_cli/test/ cli/ # CLI tests (incl. the skein test runner) ``` The suite is covered by unit and property tests across all apps; counts grow with every change, so this page doesn’t track them — run `mix test` or check CI for the current totals. ## Lexer Tests [Section titled “Lexer Tests”](#lexer-tests) The lexer test suite covers: * **Individual token types:** Each keyword, operator, and literal type has dedicated tests * **Multi-token sequences:** `let x = 42` produces the expected token list * **String handling:** Plain strings, interpolation, empty strings, escaped characters * **Position tracking:** Line and column numbers are correct across multi-line input * **Comments:** Single-line comments are properly ignored * **Error cases:** Unrecognized characters produce structured errors Example: ```elixir test "tokenizes a simple binding" do assert {:ok, tokens} = Skein.Lexer.tokenize("let x = 42") assert tokens == [ {:let, {1, 1}}, {:ident, {1, 5}, "x"}, {:eq, {1, 7}}, {:int, {1, 9}, 42}, {:eof, {1, 11}} ] end ``` ## Parser Tests [Section titled “Parser Tests”](#parser-tests) The parser test suite covers every AST construct: * **Module parsing:** Empty modules, modules with functions * **Function parsing:** With/without parameters, various return types * **Let bindings:** Simple values, expression values * **Match expressions:** Boolean matching, pattern types, multiple arms * **Operator precedence:** All operator combinations at different levels * **Operator associativity:** Left-associative chaining * **Pipe expressions:** Single and chained pipes * **Function calls:** With arguments, nested calls * **Field access:** Simple and chained field access * **String handling:** Plain strings, interpolation * **Literals:** Integers, floats, booleans * **Unary operators:** Prefix `!`, postfix `!` and `?` * **Type declarations:** Record types with typed fields * **Enum declarations:** Simple variants and variants with data * **Capability declarations:** With parameters * **Parenthesized expressions:** Grouping for precedence override * **Function references:** `&name` syntax * **Complete examples:** The full `hello.skein` program * **Error cases:** Missing tokens, unexpected tokens Example: ```elixir test "parses binary operator with correct precedence" do source = "module M { fn f(a: Int, b: Int, c: Int) -> Int { a + b * c } }" {:ok, tokens} = Lexer.tokenize(source) {:ok, %AST.Module{declarations: [f]}} = Parser.parse(tokens) assert %AST.Block{expressions: [expr]} = f.body assert %AST.BinaryOp{op: :+} = expr assert %AST.BinaryOp{op: :*} = expr.right end ``` ## CodeGen Integration Tests [Section titled “CodeGen Integration Tests”](#codegen-integration-tests) These are **full pipeline tests** — they compile Skein source to BEAM bytecode, load the module, and call its functions: * **Phase 1 acceptance tests:** `greet/1`, `add/2`, `classify/1` from `hello.skein` * **Arithmetic operations:** Addition, subtraction, multiplication * **Let bindings:** Binding preservation, sequential bindings * **Boolean operations:** Comparison operators, equality * **String operations:** Plain strings, string interpolation, multi-segment interpolation * **Match expressions:** Boolean matching with true/false arms * **File compilation:** `compile_file/1` with the example file * **Elixir interop:** `__info__(:module)`, `__info__(:functions)` Example: ```elixir test "Phase 1 acceptance: greet/1 returns interpolated greeting" do source = ~S""" module Greeter { fn greet(name: String) -> String { "Hello, ${name}!" } } """ {:module, mod} = Compiler.compile_string(source) assert mod.greet("World") == "Hello, World!" assert mod.greet("Skein") == "Hello, Skein!" end ``` ## Important Test Conventions [Section titled “Important Test Conventions”](#important-test-conventions) ### Async Settings [Section titled “Async Settings”](#async-settings) CodeGen tests use `async: false` because they load BEAM modules into the VM — a global, shared resource: ```elixir use ExUnit.Case, async: false ``` Lexer and parser tests use `async: true` since they are pure functions with no side effects. ### String Escaping [Section titled “String Escaping”](#string-escaping) Use the `~S` sigil for Skein source strings in tests to avoid Elixir interpolation conflicts: ```elixir # Good: ~S prevents Elixir from interpreting ${name} source = ~S""" module M { fn greet(name: String) -> String { "Hello, ${name}!" } } """ # Bad: Elixir would try to interpolate ${name} source = """ module M { fn greet(name: String) -> String { "Hello, ${name}!" } } """ ``` ### File Paths [Section titled “File Paths”](#file-paths) When using `compile_file/1` in tests, remember that tests run from `apps/skein_compiler/`: ```elixir # Correct: relative from apps/skein_compiler/ {:module, mod} = Compiler.compile_file("../../examples/hello.skein") ``` ## Running Tests [Section titled “Running Tests”](#running-tests) ```bash # All tests mix test # Specific test file mix test apps/skein_compiler/test/skein/parser_test.exs # Specific test by line number mix test apps/skein_compiler/test/skein/parser_test.exs:42 # With trace output mix test --trace ```