Skip to content

Contracts

What It Is

Contracts are the extension seams Dagonizer calls: nodes, runtime providers, stores, checkpoint stores, containers, handoff channels, adapters, embedders, tools, graph primitives, and registry modules.

Use this page when authoring a plugin, backend, custom node, custom store, worker container, streaming channel, or runtime integration. If Dagonizer calls into your code, the relevant shape belongs here.

How It Works

Contracts keep implementation detail outside JSON-LD. A DAG placement carries graph identity in @id, references node/DAG implementations by registered IRI, and names container roles, channels, gather strategies, or reducers. Registries and injected services bind those references to concrete code that satisfies these interfaces.

The rule is practical: implement the smallest contract Dagonizer actually needs, then register or inject it at the boundary that owns it.

Diagrams, Examples, and Outputs

Contracts appear throughout the runnable examples. The links below show default implementations and common extension points:

What It Lets You Do

The contracts reference lets applications implement extension seams without importing private internals.

Adapter contracts live at the root of src/contracts/ and ship through @studnicky/dagonizer/contracts. Single source of truth: never re-exported from a sibling module.

Code Samples

The code below lists the interfaces, option objects, and record shapes application code and plugin packages compile against.

Import

ts
import type {
  // Core dispatcher contracts
  HandoffChannelInterface,
  CheckpointStoreInterface,
  ClockProviderInterface,
  DagContainerInterface,
  EmbedderInterface,
  
ErrorConstructorType
,
ExecuteOptionsType
,
GatherExecutionType
,
GatherRecordType
,
LlmAdapterInterface, LlmClientInterface, MessageChannelInterface,
NodeInterface
,
NodeInvokerInterface,
OutcomeRecordType
,
RegistryBundleInterface, RegistryModuleInterface, RemoteStoreInterface,
RemoteStoreEndpointType
,
RemoteStoreLeaseType
,
RetryPolicyOptionsType
,
SchedulerProviderInterface, SnapshottableInterface, StateAccessorInterface, StoreInterface,
StoreSnapshotType
,
StoreSnapshotEntryType
,
SystemInfoInterface, } from '@studnicky/dagonizer/contracts'; // DagOutcomeType and DagTaskInterface ship through the root barrel import type {
DagOutcomeType
,
DagTaskInterface, } from '@studnicky/dagonizer';

NodeInterface

ts
interface 
NodeInterface
<
TState
extends NodeStateInterface = NodeStateInterface,
TOutput
extends string = string,
> { readonly
name
: string;
readonly
outputs
: readonly
TOutput
[];
readonly
outputSchema
:
Record
<
TOutput
,
SchemaObjectType
>;
readonly
timeout
:
Timeout
;
execute
(
batch
:
Batch
<
TState
>,
context
:
NodeContextType
):
Promise
<
RoutedBatchType
<
TOutput
,
TState
>>;
destroy
?():
Promise
<void>;
validate
?():
ValidationResultType
;
}

The contract every application node implements. Nodes are stateless; they mutate state and route to a named output. They never throw: caught errors route to 'error' or another declared error output.

outputSchema is a mandatory per-output-port JSON Schema 2020-12 record describing the state delta each port guarantees. Every declared output port in outputs must have an entry. Schemas are partial over state — they validate the fields the node writes; do not set additionalProperties: false. MonadicNode provides a passthrough default ({ type: 'object' } per port); concrete nodes should override with real schemas.

timeout is a per-node wall-clock budget expressed as a Timeout value (Timeout.ofMs(n) or Timeout.none()). When set to a non-none value, the engine derives a child AbortController from the run's signal and schedules an abort after the budget. On expiry, NodeTimeoutError is thrown and the run is marked failed. The MonadicNode base class defaults to Timeout.none(); nodes that do not extend it should omit the field (treated as Timeout.none() by the engine).

ExecuteOptionsType

ts
interface ExecuteOptionsType {
  readonly 
signal
?: AbortSignal;
readonly
deadlineMs
?: number;
}

Dagonizer.execute and Dagonizer.resume accept this as their third argument. Signal.compose (from @studnicky/signal) folds the two fields into a single signal.

ClockProviderInterface

ts
interface ClockProviderInterface {
  
hrtime
(): bigint;
}

Backend for the Clock singleton. Implement to swap time sources (typically in tests via VirtualClockProvider from @studnicky/dagonizer/testing).

SchedulerProviderInterface

ts
interface SchedulerProviderInterface {
  
after
(
delayMs
: number,
options
?:
AbortableOptionsType
):
Promise
<void>;
at
(
atMs
: number,
options
?:
AbortableOptionsType
):
Promise
<void>;
every
(
intervalMs
: number,
options
?:
AbortableOptionsType
):
AsyncIterable
<void>;
cancelAll
(): void;
}

SchedulerProviderInterface is the backend contract; implement it to swap in a custom scheduler. Scheduler.current() returns the active SchedulerProviderInterface. Production uses RealTimeScheduler; tests install VirtualScheduler from @studnicky/dagonizer/testing.

StateAccessorInterface

ts
interface StateAccessorInterface {
  
get
(
state
: object,
path
: string): unknown;
set
(
state
: object,
path
: string,
value
: unknown): void;
}

Path resolver used for scatter source reads, state-mapping input copies, and gather writes. Default implementation: DottedPathAccessor in runtime/. Pass a custom implementation via new Dagonizer({ accessor }).

SnapshottableInterface

ts
interface SnapshottableInterface {
  
snapshot
():
Promise
<
StoreSnapshotType
>;
restore
(
snapshot
:
StoreSnapshotType
):
Promise
<void>;
}

The capability checkpointing depends on. Checkpoint.capture(dag, result, { stores }) and ckpt.restoreStores(map) take Record<string, SnapshottableInterface>, so a non-KV backing (RDF triple store, vector index) can ride along in a checkpoint without implementing the key-value surface. StoreInterface extends SnapshottableInterface. The StoreSnapshotType / StoreSnapshotEntryType envelopes live with it. See Store for the envelope shape and BaseStore.

CheckpointStoreInterface

ts
interface CheckpointStoreInterface {
  
save
(
key
: string,
json
: string):
Promise
<void>;
load
(
key
: string):
Promise
<string | null>;
delete
(
key
: string):
Promise
<void>;
}

Persistence backend for checkpoints. ckpt.persist(store, key) and Checkpoint.recall(store, key) compose the codec with the store. Reference impl: MemoryCheckpointStore. See persistence for a Postgres example.

EmbedderInterface

ts
interface EmbedderInterface {
  readonly 
id
: string;
readonly
displayName
: string;
readonly
dimensions
: number;
embed
(
text
: string):
Promise
<readonly number[]>;
embedBatch
(
texts
: readonly string[]):
Promise
<readonly (readonly number[])[]>;
probe
():
Promise
<boolean>;
connect
():
Promise
<void>;
disconnect
():
Promise
<void>;
}

Produces a fixed-dimensionality vector for a text input. Plugins implement this (typically by extending BaseEmbedder from @studnicky/dagonizer/adapter) to swap embedding backends. The adapter cascade pattern applies: register multiple EmbedderInterfaces, probe at runtime, pick the first available.

MemberDescription
idProvider identifier ('ollama', 'gemini-api', etc.)
displayNameHuman-readable label for logs and UI
dimensionsOutput vector dimensionality. Applications verify match against pre-computed corpus embeddings
embed(text)Embed a single text, returning a number[] of length dimensions. Throws LlmError on failure
embedBatch(texts)Batch convenience. Default in BaseEmbedder calls embed() in series
probe()Quick availability check. Must not throw; returns false so a cascade can route around the embedder
connect() / disconnect()Per-session lifecycle hooks

RetryPolicyOptionsType / ErrorConstructorType

ts
type 
ErrorConstructorType
= new (...
args
: never[]) => Error;
interface RetryPolicyOptionsType { readonly
maxAttempts
?: number;
readonly
strategy
?:
BackoffStrategyType
;
readonly
baseDelay
?: number;
readonly
maxDelay
?: number;
readonly
multiplier
?: number;
readonly
jitterFactor
?: number;
readonly
retryOn
?: readonly
ErrorConstructorType
[];
readonly
abortOn
?: readonly
ErrorConstructorType
[];
}

Construction options for RetryPolicy. retryOn and abortOn are checked via instanceof. Supply error classes, not error names.

Store / StoreSnapshotType / StoreSnapshotEntryType

The store contracts ship through @studnicky/dagonizer/contracts alongside the other adapter interfaces. Full documentation (concurrency contract, BaseStore authoring guide, StoreErrorClassification taxonomy) lives in Reference: Store.

ts
import type { StoreInterface, 
StoreSnapshotType
,
StoreSnapshotEntryType
} from '@studnicky/dagonizer/contracts';

See Shared state for the decision matrix and usage patterns.

RemoteStore / RemoteStoreEndpointType / RemoteStoreLeaseType

Extension of Store for network-backed or replicated store plugins. Implements the same Store surface plus endpoint, acquireLease, releaseLease, and health for distributed coordination.

ts
import type { RemoteStoreInterface, 
RemoteStoreEndpointType
,
RemoteStoreLeaseType
} from '@studnicky/dagonizer/contracts';

See Reference: Store for the full interface and Shared state for the authoring guide.

DagContainerInterface

ts
interface DagContainerInterface {
  
runDag
(
task
: DagTaskInterface,
options
?: { readonly
relay
?: ObserverRelayInterface }):
Promise
<
DagOutcomeType
>;
destroy
?():
Promise
<void>;
}

Adapter contract for running an embedded DAG or DAG-body scatter in an isolate (worker thread, forked child, spawned process, Web Worker, or remote host). Bound to the dispatcher via DagonizerOptionsType.containers keyed by logical role name. On a dispatcher with a non-empty containers registry, a declared-but-unbound role throws DAGError at registerDAG time. A pure in-process dispatcher (empty containers) treats declared roles as inert and runs every body in-process.

runDag must preserve the child DAG boundary: the task carries the selected DAG IRI, placement path, state snapshot, timeout, and execution context; the outcome returns terminal output, terminal state snapshot, collected errors, and intermediates. Transport failures, host crashes, and serialization errors are returned as collected errors in DagOutcomeType.errors with recoverable: false.

destroy() is optional. Implement it to release pool resources when the dispatcher shuts down.

HandoffChannelInterface

ts
interface HandoffChannelInterface {
  
publish
(
handoff
:
DAGHandoffType
):
Promise
<void>;
destroy
?():
Promise
<void>;
}

Adapter contract for publishing completed-DAG hand-off envelopes to a downstream transport (queue, message bus, or loopback store). Bound via DagonizerOptionsType.channels keyed by terminal placement name. Implementations must not throw out of the dispatcher; any internal transport error is the implementation's responsibility. InMemoryChannel in @studnicky/dagonizer/channels is the reference implementation.

MessageChannelInterface

ts
interface MessageChannelInterface {
  
send
(
message
:
BridgeMessageType
): void;
onMessage
(
handler
: (
message
:
BridgeMessageType
) => void): void;
close
(): void;
}

Duplex channel contract between a parent dispatcher and a DagHost. send is fire-and-forget (does not throw). onMessage registers the inbound handler (replaces any previous handler). close severs both directions; outstanding send calls are silently dropped. Implementations include LoopbackChannel (in-memory, for testing), MessagePortChannel (worker threads), IpcChannel (child process), and NdjsonChannel (stdio, polyglot hosts).

RegistryModuleInterface / RegistryBundleInterface

ts
interface RegistryBundleInterface {
  readonly 
bundle
:
DispatcherBundleType
<NodeStateInterface>;
readonly
registryVersion
: string;
readonly
restoreState
:
CheckpointRestoreAdapterInterface
<NodeStateInterface>;
destroy
?():
Promise
<void>;
} interface RegistryModuleInterface {
instantiate
(
servicesConfig
:
JsonObjectType
):
Promise
<RegistryBundleInterface>;
}

RegistryModuleInterface is the default export shape of a registry module loaded by DagHost via dynamic import. instantiate receives the opaque servicesConfig JSON from the init message and returns a fully initialised RegistryBundleInterface.

RegistryBundleInterface bundles the node+DAG registry (bundle), the semantic version for the init ↔ ready handshake (registryVersion), and the state restore factory (restoreState). Node instances are constructed inside the registry module (with their constructor-injected dependencies); the constructed instances cross no isolate boundary — each isolate builds its own graph via its registry module.

DagOutcomeType

ts
interface DagOutcomeType {
  readonly 
terminalOutput
: string;
readonly
errors
: readonly
NodeErrorWireType
[];
readonly
stateSnapshot
:
JsonObjectType
| null;
readonly
intermediates
: readonly
ExecutorIntermediateType
[];
}

Result returned by DagContainerInterface.runDag() after a child DAG completes in an isolate. terminalOutput is the routing output the child resolved to. stateSnapshot is the terminal child state snapshot (null when the container cannot produce one, e.g. transport failure); the parent calls cloneState.applySnapshot(stateSnapshot) when non-null. intermediates are per-node results forwarded to the parent execution stream.

DagTaskInterface

ts
interface DagTaskInterface {
  
dagName
: string;
placementPath
: string[];
correlationId
: string;
timeout
:
Timeout
;
state
: NodeStateInterface;
context
:
NodeContextType
;
toRequest
():
ExecutionRequestType
;
}

Engine-side descriptor of a contained DAG execution. Carries a live seeded child clone (state, typed at the NodeStateInterface contract because the engine is heterogeneous-state) for the in-process path. Isolating containers call toRequest() to snapshot the clone into a wire-safe ExecutionRequest. correlationId is a dispatcher-monotonic id (no randomness). timeout is a Timeout; Timeout.none() means no per-task budget applies.

SystemInfoInterface

ts
interface SystemInfoInterface {
  
recommendedWorkerCount
(
config
:
RecommendedWorkerCountConfigType
): number;
}

Host-environment probe for pool sizing recommendations. Implementations are environment-specific (Node os.availableParallelism() + os.totalmem(); Web navigator.hardwareConcurrency). The recommended count follows the quadrascope formula: clamp(parallelism − mainThreadReservation, minimumWorkerCount, maximumWorkers), optionally further clamped by memoryPerWorkerBytes.

GatherExecutionType / GatherRecordType / OutcomeRecordType

These contracts ship through @studnicky/dagonizer/contracts for use by custom gather strategy and outcome reducer implementations. See Reference: Core for the full authoring guide.

ts
import type { 
GatherExecutionType
,
GatherRecordType
,
OutcomeRecordType
} from '@studnicky/dagonizer/contracts';

GatherRecordType<TState> carries producer results into a GatherNode: source, index, item, output, terminalOutcome, result, and cloneState. GatherExecutionType<TState> is the invocation context handed to GatherStrategy.finalize: it provides records, the live parent state, the accessor, and invoker (a NodeInvoker; used by the custom strategy via invoker.invokeNode(nodeIri)). OutcomeRecordType is the per-clone summary handed to OutcomeReducer.reduce: index, output, and terminalOutcome.

LlmAdapterInterface / LlmClientInterface

ts
interface LlmAdapterInterface {
  readonly 
id
: string;
readonly
displayName
: string;
readonly
capabilities
:
AdapterCapabilitiesType
;
chat
(
request
:
ChatRequestType
):
Promise
<
ChatResponseType
>;
chatStream
(
request
:
ChatRequestType
,
sink
:
StreamSinkInterface
<
ChatStreamChunkType
>):
Promise
<
ChatResponseType
>;
connect
():
Promise
<void>;
disconnect
():
Promise
<void>;
probe
():
Promise
<boolean>;
} interface LlmClientInterface {
chat
(
request
:
ChatRequestType
):
Promise
<
ChatResponseType
>;
}

LlmAdapterInterface is the transport contract every LLM provider adapter implements. Provider packages extend BaseAdapter from @studnicky/dagonizer/adapter to inherit retry and error classification. BaseAdapterOptionsType also carries cross-cutting options every adapter accepts: systemPrompt, a default directive the base injects as the leading system message of any request that carries none (never overriding an explicit system turn), timeoutMs (default 60_000), a per-request deadline, optional circuitBreaker / tokenBucket guards, and optional timing for substrate adapter.chat.* / adapter.chatStream.* events. An expired deadline surfaces as a TIMEOUT classification so a cascade falls through instead of hanging. LlmClientInterface is the minimal chat surface pattern bases accept — any LlmAdapterInterface satisfies it. Pattern bases that need capability metadata (e.g. tool-call support) accept the full LlmAdapterInterface directly.

chat() is the buffered call: it resolves once with the complete ChatResponseType. chatStream(request, sink) additionally pushes incremental ChatStreamChunkType ({ delta }) values to sink as the response is generated, while still resolving with the same fully-assembled ChatResponseType — the sink is a pure observation channel, not an alternate return path. BaseAdapter's default performChatStream is buffered: it calls chat() internally and pushes exactly one chunk carrying the full response text, so every adapter satisfies the streaming contract even without a streaming backend. Anthropic, the Gemini API adapter, and the Ollama, Groq, Cerebras, Mistral, and OpenRouter adapters override performChatStream to push real per-token deltas parsed from a server-sent-events response body. gemini-nano streams via the in-browser LanguageModel session's promptStreaming() async iterable; web-llm streams via the @mlc-ai/web-llm engine's own stream. Tool-bearing requests (request.tools.length > 0) use the buffered default because partial tool-call JSON is unsafe to parse mid-stream. chatStream is single-attempt and bounded by the same abort+timeout deadline (timeoutMs). sink.push() delivery is best-effort: a rejecting sink never fails the call. See Adapters for the full per-provider streaming reference and ReAct agent: live token streaming for a working CallModelNode + sink example.

NodeInvokerInterface

ts
interface NodeInvokerInterface {
  
invokeNode
(
nodeIri
: string):
Promise
<void>;
}

Typed contract for dispatching a registered node back through the engine. Lives on GatherExecutionType.invoker; used exclusively by custom gather strategies to invoke the registered node IRI in GatherConfig.customNode. Custom strategies access it via execution.invoker.invokeNode(nodeIri).

Details for Nerds

Contracts are intentionally narrow. A node contract does not know how the dispatcher stores registries. A store contract does not know how checkpoints serialize. A channel contract does not know which host receives the handoff. Each seam receives only the methods Dagonizer needs to call.

That keeps plugin packages portable: implement the contract, register or inject the implementation, and let JSON-LD continue describing topology by IRI.

Watched over by the Order of Dagon.