Skip to main content
@obversa/runtime lets a package define a graph type that the runtime does not ship. Write graph behavior as pure code. The contract gives the graph type frozen data and graph lookups. It does not give file, model, process, clock, or storage services. An outside graph type is trusted package code. It can import and use effects by itself. The conformance kit checks public behavior. It does not stop effects.

Start with the example

The checked-in example defines a two-step graph, runs the public conformance kit, compiles its definition, and resolves its plan. From an Obversa checkout, run:
Copy examples/packages/custom-graph.ts when you start a graph type. The example uses only @obversa/runtime public exports.

Source

Define the graph

A GraphDefinition is JSON data with these fields:
  • A stable graph id and positive definitionVersion.
  • Graph data.
  • Nodes with stable id values and data.
  • Edges with stable id, source, target, and data values.
compileGraph(graphType, definition) validates the definition before it calls the graph type. It returns a frozen copy with a canonical JSON value and a SHA-256 digest. Invalid definitions throw GraphValidationError. Common errors include empty identifiers, duplicate node or edge identifiers, and an edge that names a node that does not exist. No node can run from this graph layer.

Implement GraphType

A graph type has a stable kind, a positive version, and one compile method. compile receives the validated, frozen definition and a GraphKernel. The kernel gives graph data and neighbor lookups. It does not give runtime services. compile returns these pure operations: decide returns zero or more dispatch commands, or exactly one pause, complete, or fail command. A dispatch asks the executor to start new work. An empty decision means start nothing new. The executor accepts it only while a recorded attempt is still in flight. Without in-flight work, the executor fails instead of asking for the same empty decision forever. position is the stable logical identity and location of one requested node occurrence. Positions must be unique within one decision. The same node can appear more than once when each occurrence has a different position. After a dispatch occurrence is recorded in the event history, decide must not return it again. A later event can make the same node dispatchable as a new occurrence with its own position. The input state and event are copied and frozen before reduce or decide receives them. These methods must return JSON data. The same definition and events must give the same state and commands. GraphBindings<Requirements> maps a precise graph requirement to a host binding type. A required memory requirement maps to a required Memory binding. An unused memory requirement maps to a type with no memory binding. This graph layer does not create, inject, or execute a binding.

Describe the graph

describe() declares data for a host to inspect before execution. It includes phases, nodes, edges, input and output contracts, policies, execution lanes, requested permissions, and bounds. Each bound is either known or unknown. Use a known bound only when the graph can calculate the value. Use an unknown bound with a reason when it cannot. Do not invent a maximum or concurrency value. The description must name every definition node once in a phase. Its nodes list preserves definition node declaration order. The compiler supplies the edges list in definition edge declaration order. Each node lane must exist in executionLanes. A node’s phaseId must name the same phase that lists that node.

Validate a description

Use validateGraphDescription(unknown) to validate a graph description that you received outside compileGraph. It returns a frozen validated description. Invalid data, including JSON nested more than 256 levels, throws GraphValidationError.

Conformance

runGraphTypeConformance checks a fixture without a test framework. The fixture declares the expected initial state and command, then the expected state and command after every event prefix. The kit compiles the graph type in separate instances and calls each operation more than once. It checks that the initial state, every event-prefix state, every ordered command, and the declared bounds stay the same. It also checks invalid definitions and preserves the caller’s definition and events. The kit counts all dispatch commands in the supplied trace and the largest dispatch set in one decision. A declared dispatch maximum or fan-out maximum cannot be below those observed values. The trace does not show which attempts overlap at runtime, so the kit does not infer maxConcurrency. The graph must declare that cap from its own rules or report it as unknown. Each expected decision contains only new requests. The kit rejects a dispatch position that appears again in a later expected decision. The executor applies the same check to durable dispatch events before it starts work. When the final expected decision is exactly complete, the observed total must meet a known dispatch minimum; partial, empty, paused, and failed endings do not prove a minimum.

Limits

This contract does not schedule nodes, store runs, provide built-in graph forms, or execute work on another machine. A host and later runtime layers do those jobs.