@quarry-systems/drift-contracts
v0.0.2-alpha.6
Published
Zod validation schemas and contracts for Drift (Managed Cyclic Graph)
Maintainers
Readme
@quarry-systems/drift-contracts
TypeScript type definitions and Zod validation schemas for the Drift (Managed Cyclic Graph) ecosystem.
Overview
drift-contracts provides the foundational type system and validation schemas used across all Drift packages. It defines interfaces for graphs, nodes, edges, plugins, services, and infrastructure adapters, ensuring type safety and consistency throughout the ecosystem.
Installation
npm install @quarry-systems/drift-contractsFeatures
- ✅ Comprehensive Type System: Complete TypeScript definitions for all Drift concepts
- ✅ Zod Validation: Runtime validation schemas for configuration and data
- ✅ Plugin Contracts: Interfaces for execution and infrastructure plugins
- ✅ Service Contracts: Type-safe service definitions and injection
- ✅ AI/LLM Types: Complete types for LLM adapters, tools, and streaming
- ✅ Infrastructure Adapters: Contracts for queues, stores, secrets, retrieval, and vectors
- ✅ Zero Runtime Dependencies: Pure types with minimal overhead (only Zod)
Core Concepts
Graph Structure
import type { GraphDef, NodeDef, EdgeDef } from '@quarry-systems/drift-contracts';
const graph: GraphDef = {
id: 'my-graph',
nodes: {
start: { id: 'start', label: 'Start Node' },
end: { id: 'end', label: 'End Node' },
},
edges: [
{ id: 'start->end', source: 'start', target: 'end', guards: ['any'] },
],
startNodes: ['start'],
};Context and Execution
import type { Ctx, Action, Guard } from '@quarry-systems/drift-contracts';
// Context structure
const ctx: Ctx = {
runId: 'run-123',
data: { userId: '456' },
global: { apiKey: 'secret' },
injected: { timestamp: Date.now() },
errors: [],
events: []
};
// Guard function
const guard: Guard = {
id: 'hasUserId',
// prevResult is the previous node's handler output (if any), so guards
// can route on handler results without threading state through ctx.data.
evaluate: (ctx, prevResult) => {
if (ctx.data.userId === undefined) {
return { ok: false, reason: 'missing userId' };
}
return { ok: true };
}
};
// Action function
const action: Action = async (ctx) => {
return {
...ctx,
data: {
...ctx.data,
processed: true
}
};
};Guard Interface
Guards are pure predicates evaluated against the context (and optionally the previous node's result) to decide whether an edge can be traversed.
import type { Guard, EvalResult, MaybePromise } from '@quarry-systems/drift-contracts';
interface Guard<TCtx = Ctx> {
id: string;
description?: string;
priority?: number;
timeoutMs?: number;
evaluate(ctx: TCtx, prevResult?: unknown): MaybePromise<EvalResult>;
}prevResult parameter — Optional second argument to evaluate. When the
graph edge being evaluated follows a node execution, the runtime passes that
node's handler output as prevResult. This lets guards make routing decisions
directly from handler results without having to merge them into ctx.data
first.
// Route on handler output without polluting ctx.data
const isSuccess: Guard = {
id: 'isSuccess',
evaluate: (_ctx, prevResult) => {
const r = prevResult as { status?: string } | undefined;
return r?.status === 'ok'
? { ok: true }
: { ok: false, reason: 'handler did not return ok' };
}
};prevResult is unknown — cast it to the shape your handlers actually
return. When no previous result exists (e.g., the first edge of a run),
prevResult is undefined.
Edge Definition
import type { EdgeDef } from '@quarry-systems/drift-contracts';
interface EdgeDef<TCtx extends Ctx = Ctx> {
id: string;
source: string;
target: string;
guards?: Guard<TCtx>[];
onTransit?: (Action<TCtx> | string)[];
priority?: number;
/**
* Max times this edge can be traversed per run.
* Omit or `0` = unlimited.
*/
maxTraversals?: number;
/**
* Intrinsic meter memberships stamped at `.meter()` declaration time.
* Each entry is a meter id that claims this edge as a member; read by the
* per-meter enforcement guard's wrap-predicate.
*/
intrinsicMeters?: string[];
}maxTraversals field — Hard cap on how often a given edge is allowed to
fire within a single run. The runtime increments a per-edge counter each time
the edge is traversed and skips the edge during selection once the counter
reaches maxTraversals. Omit the field (or set it to 0) for unlimited
traversals. Useful for bounding remediation loops or retry cycles without
writing a manual counter into ctx.data.
// Retry at most 3 times, then fall through to the failure edge
const retryEdge: EdgeDef = {
id: 'verify->execute',
source: 'verify',
target: 'execute',
guards: [notSuccessGuard],
maxTraversals: 3
};Plugin System
import type { Plugin, NodeHandler } from '@quarry-systems/drift-contracts';
// Node handler
const myHandler: NodeHandler = async (node, ctx, meta) => {
// Process the node
return {
...ctx,
data: {
...ctx.data,
result: 'processed'
}
};
};
// Plugin definition
const myPlugin: Plugin = {
name: 'my-plugin',
version: '1.0.0',
description: 'My custom plugin',
nodes: {
'my-node-type': myHandler
}
};Service Contracts
import type { ServiceContract, ServiceDefinition } from '@quarry-systems/drift-contracts';
// Define a service contract
const MyServiceContract: ServiceContract = {
name: 'my-service',
version: '1.0.0',
capabilities: ['read', 'write'],
methods: {
getData: {
input: { id: 'string' },
output: { data: 'object' }
}
}
};
// Service definition
const myService: ServiceDefinition = {
contract: MyServiceContract,
scope: 'run',
factory: (ctx) => ({
getData: async (id: string) => ({ data: { id } })
})
};Type Categories
Core Types
- Context:
Ctx,AnyCtx,MaybePromise,EvalResult - Graph:
GraphDef,NodeDef,EdgeDef,GraphLimits - Behaviors:
Guard,Action(Rule/GraphRuleare still exported for back-compat but no longer wired by drift-core) - Events:
GraphEvent,NodeEvent,GraphEventHandler,NodeEventHandler
Graph version pinning (alpha.3+):
import type {
GraphVersion,
GraphVersionResolution,
GraphVersionResult,
GraphVersionComparator,
} from '@quarry-systems/drift-contracts';GraphVersion=string. Opaque; may be an explicit consumer-supplied value (semver/date/SHA/build-id) or a structural-hash fallback (sha256-v1:<hex>).GraphVersionResolution={ version: GraphVersion; source: 'explicit' | 'structural-hash'; structuralHash: string }. Captured at Manager construction; persisted on every snapshot. Whensource==='structural-hash',version === structuralHash.GraphVersionResult='match' | 'compatible' | 'mismatch'. Comparator outcome. Adding outcomes is a SemVer-major change.GraphVersionComparator=(snapshot: GraphVersionResolution, current: GraphVersionResolution) => GraphVersionResult. Pure, sync, side-effect-free. Argument order reads "is this snapshot compatible with current?"
Two new event variants on the RunEvent union: RunResumeGraphVersionMissing (legacy snapshots) and RunResumeGraphVersionWarning ('compatible' outcomes). Both carry full GraphVersionResolution payloads for triage.
AI/LLM Types
- Messages:
ChatMessage,MessageRole,ResponseFormat - Tools:
ToolDefinition,ToolCall - Requests:
LLMRequest,LLMResponse,LLMUsage,LLMError - Streaming:
LLMStreamEvent - Adapters:
LLMAdapter,EmbeddingAdapter - Schema:
MCGJsonSchema
Plugin Types
- Core:
Plugin,NodeHandler,PluginMetadata - Manifest:
PluginManifest,PluginType,PluginCapabilities - Validation:
PluginValidationResult
Infrastructure Adapters
Queue Adapter
import type { QueueAdapter, QueueJob, JobProcessor } from '@quarry-systems/drift-contracts';Store Adapter
import type {
RunStoreAdapter,
ArtifactStore,
RunSnapshot,
RunMetadata
} from '@quarry-systems/drift-contracts';Secrets Adapter
import type { SecretsAdapter, SecretRef } from '@quarry-systems/drift-contracts';Retrieval Adapter
import type {
RetrievalAdapter,
Document,
Chunk,
ChunkMatch
} from '@quarry-systems/drift-contracts';Vector Adapter
import type {
VectorAdapter,
VectorItem,
VectorMatch
} from '@quarry-systems/drift-contracts';Service System
- Contracts:
ServiceContract,ServiceCapabilities,VersionCompatibility - Injection:
ServiceDefinition,ServiceRegistry,ServiceScope - Validation:
ServiceValidationResult
Middleware
import type {
Middleware,
NodeStartEvent,
NodeEndEvent,
ServiceCallStartEvent
} from '@quarry-systems/drift-contracts';Error Handling
import type {
ExecutionError,
TraceEntry,
ExecutionTrace,
DebugOptions
} from '@quarry-systems/drift-contracts';Licensing
import type {
LicenseTier,
LicenseStatus,
LicenseFile,
LicenseOptions
} from '@quarry-systems/drift-contracts';Usage Examples
Building Type-Safe Graphs
import type { GraphDef, NodeDef, Ctx } from '@quarry-systems/drift-contracts';
function createWorkflow(): GraphDef<Ctx, { userId: string }> {
return {
id: 'user-workflow',
nodes: {
fetch: {
id: 'fetch',
label: 'Fetch User',
meta: { http: { url: '/api/users/${injected.userId}' } },
},
process: {
id: 'process',
label: 'Process Data',
},
},
edges: [
{ id: 'fetch->process', source: 'fetch', target: 'process', guards: ['any'] },
],
startNodes: ['fetch'],
};
}Creating Custom Plugins
import type { Plugin, NodeHandler, Ctx } from '@quarry-systems/drift-contracts';
const delayHandler: NodeHandler = async (node, ctx, meta) => {
const ms = node.meta?.delay || 1000;
await new Promise(resolve => setTimeout(resolve, ms));
return ctx;
};
export const delayPlugin: Plugin = {
name: 'delay-plugin',
version: '1.0.0',
description: 'Adds delay nodes',
nodes: {
'delay': delayHandler
}
};Implementing Adapters
import type { SecretsAdapter } from '@quarry-systems/drift-contracts';
class EnvSecretsAdapter implements SecretsAdapter {
async get(key: string): Promise<string | null> {
return process.env[key] || null;
}
async set(key: string, value: string): Promise<void> {
process.env[key] = value;
}
async delete(key: string): Promise<void> {
delete process.env[key];
}
async list(): Promise<string[]> {
return Object.keys(process.env);
}
}Package Structure
drift-contracts/
├── src/
│ ├── ai.ts # AI/LLM types
│ ├── behaviors.ts # Guard, Action (Rule still exported for back-compat)
│ ├── context.ts # Context and state types
│ ├── errors.ts # Error and debugging types
│ ├── events.ts # Event system types
│ ├── graph.ts # Graph structure types
│ ├── license.ts # Licensing types
│ ├── manifest.ts # Plugin manifest types
│ ├── middleware.ts # Middleware types
│ ├── plugin.ts # Plugin system types
│ ├── queue.ts # Queue adapter types
│ ├── retrieval.ts # Retrieval adapter types
│ ├── secrets.ts # Secrets adapter types
│ ├── service-contract.ts # Service contract types
│ ├── service-injection.ts # Service injection types
│ ├── store.ts # Store adapter types
│ ├── vector.ts # Vector adapter types
│ └── index.ts # Main exportsExecute-Phase Middleware Hooks
Two additional hooks on the Middleware interface fire during the execute phase
of a node, giving middleware the ability to propose context patches in addition
to observing execution.
import type {
NodeBeforeExecuteEvent,
NodeAfterExecuteEvent,
Middleware,
} from '@quarry-systems/drift-contracts';NodeBeforeExecuteEvent
Fired after onEnter is committed but before the node's execute actions run.
| Field | Type | Description |
|-------|------|-------------|
| nodeId | string | Node identifier |
| runId | string | Run identifier |
| graphId | string | Graph identifier |
| context | TCtx | Current context state |
| timestamp | number | Event timestamp (ms since epoch) |
| propose | (patch) => void | Emit a patch into the Tier-2 patch buffer |
NodeAfterExecuteEvent
Fired after the node's execute actions run but before edge selection.
| Field | Type | Description |
|-------|------|-------------|
| nodeId | string | Node identifier |
| runId | string | Run identifier |
| graphId | string | Graph identifier |
| context | TCtx | Context state after execute actions |
| timestamp | number | Event timestamp (ms since epoch) |
| prevResult | unknown | Last execute action's return value; undefined if no execute actions ran |
| propose | (patch) => void | Emit a patch into the Tier-2 patch buffer |
propose() signature
propose(patch: Omit<Patch, 'ts' | 'by'> & { by?: string }): voidpropose() enqueues a patch into the runtime's Tier-2 patch buffer. Patches are
committed into ctx at phase boundaries — they are not immediately visible to
later middleware in the same hook chain.
Observer shape preserved
Both hooks return void | Promise<void>. Mutation is done exclusively through
propose(); the hook return value is ignored by the runtime. This keeps the
Middleware interface consistent with its observer-only contract: hooks observe,
and propose() is the explicit opt-in for mutation.
Both hooks are optional on the Middleware interface.
const auditMiddleware: Middleware = {
name: 'audit',
version: '1.0.0',
async onBeforeNodeExecute(event) {
// Inject a timestamp patch before execute runs
event.propose({ op: 'set', path: ['_auditStart', event.nodeId], value: event.timestamp });
},
async onAfterNodeExecute(event) {
// Log or propose a patch based on the execute result
console.log(`${event.nodeId} prevResult:`, event.prevResult);
},
};Runtime Events
RuntimeEvent is part of the DriftEvent union emitted on the graph's unified
event bus. Subscribe via graph.event() or graph.__eventBus.on().
import type { RuntimeEvent, DriftEvent } from '@quarry-systems/drift-contracts';RuntimeEvent variants
| Type | Fields |
|------|--------|
| MeterExceeded | runId, nodeId, meter: string, current: number, limit: number, timestamp: number |
| WaitSuspended | runId, nodeId, keys: string[], timestamp: number, timeoutAt?: number |
| WaitResolved | runId, key: string, timedOut: boolean, timestamp: number |
| ServiceRebuilt | runId, key: string, timestamp: number |
| RunSuspended | runId, nodeId, keys: string[] |
| RunResumed | runId, nodeId |
Subscription examples
// Via graph.event() — typed DriftEvent handler
graph.event('MeterExceeded', (e) => {
console.warn(`Meter "${e.meter}" exceeded: ${e.current}/${e.limit} at node ${e.nodeId}`);
});
// Via graph.__eventBus.on() — lower-level subscription
graph.__eventBus.on('WaitSuspended', (e) => {
console.log(`Run ${e.runId} suspended at ${e.nodeId}, waiting for keys: ${e.keys.join(', ')}`);
if (e.timeoutAt) {
console.log(` Timeout at: ${new Date(e.timeoutAt).toISOString()}`);
}
});
graph.__eventBus.on('WaitResolved', (e) => {
if (e.timedOut) {
console.warn(`Wait key "${e.key}" timed out in run ${e.runId}`);
}
});Meter Types
Resource meters are declarative accumulators with limits. The runtime
auto-composes a metersOk guard onto every non-essential edge and fires
MeterExceeded when any meter breaches its limit.
import type { MeterSource, MeterDef, MetersPolicy } from '@quarry-systems/drift-contracts';MeterSource
Discriminated union describing where the meter reads its current value.
type MeterSource =
| { kind: 'metrics'; key: string } // reads metrics plugin's total(key)
| { kind: 'timer' } // reads ctx.timer.elapsed (requires timer: true)
| { kind: 'builtin'; name: 'steps' } // reads ctx.data.__currentStepMeterDef
interface MeterDef {
limit: number; // Maximum allowed value; breach fires MeterExceeded
source: MeterSource; // Where the current value comes from
description?: string; // Human description for logs/errors
}MetersPolicy
type MetersPolicy = Record<string, MeterDef>;Example
const meters: MetersPolicy = {
// Limit to 50 LLM token calls (tracked via metrics plugin)
llmCalls: {
limit: 50,
source: { kind: 'metrics', key: 'llm.calls' },
description: 'Maximum LLM calls per run',
},
// Limit wall-clock time to 5 minutes (requires Manager option timer: true)
wallTime: {
limit: 5 * 60 * 1000,
source: { kind: 'timer' },
description: '5-minute wall-clock budget',
},
// Limit traversal steps
steps: {
limit: 200,
source: { kind: 'builtin', name: 'steps' },
description: 'Maximum graph steps',
},
};Context Extensions
CtxTimer
A checkpoint-aware timer for tracking wall-clock time minus paused intervals.
Enabled via the Manager option timer: true and available as ctx.timer.
interface CtxTimer {
readonly elapsed: number; // Total elapsed ms excluding paused intervals
pause(): void; // Pause the timer (e.g., waiting for user input)
resume(): void; // Resume the timer
readonly paused: boolean; // Whether currently paused
}// Inside a node action
const budget = 5 * 60 * 1000; // 5 minutes
if (ctx.timer && ctx.timer.elapsed > budget) {
throw new Error('Run exceeded time budget');
}WaitOptions
Options passed to ctx.waitForInjection().
interface WaitOptions {
timeoutMs?: number; // Wall-clock timeout in ms; on expiry, ctx.injected.__waits[key] = { __timedOut: true }
mode?: 'all'; // Reserved; v1 only supports all-satisfied resume
}InjectedWaits
Reserved sub-namespace on ctx.injected written by Manager.inject(runId, key, value).
interface InjectedWaits {
[key: string]: unknown | { __timedOut: true };
}Keys use a flat, dot-safe convention: 'foo.bar' is a literal key, not a
nested path. A timed-out key has the shape { __timedOut: true }.
ctx.waitForInjection and ctx.injected.__waits
// Inside a node action — register a wait for an external event
ctx.waitForInjection?.('approval', { timeoutMs: 60_000 });
// The NEXT node reads the resolved value (or timeout sentinel)
const result = (ctx.injected as any).__waits?.['approval'];
if (result?.__timedOut) {
// Handle timeout
} else {
// Use result
}waitForInjection uses mark-and-return semantics: the call registers the wait
and returns immediately. Suspension happens at the next onAfterNodeExecute
phase. The injected value is consumed by the next node, not the node that
called waitForInjection.
Related Packages
- @quarry-systems/drift-core - Core graph execution engine
- @quarry-systems/drift-ai-core - AI agent functionality
- @quarry-systems/drift-cli - Command-line interface
- @quarry-systems/drift-testing - Testing utilities
Official Plugins
@quarry-systems/drift-http- HTTP client@quarry-systems/drift-timer- Timers and scheduling@quarry-systems/drift-openai- OpenAI integration@quarry-systems/drift-secrets- Secrets management@quarry-systems/drift-store-sqlite- SQLite storage@quarry-systems/drift-vector-chroma- ChromaDB vectors
TypeScript Configuration
For best results, use these TypeScript compiler options:
{
"compilerOptions": {
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"moduleResolution": "node",
"resolveJsonModule": true
}
}Contributing
This package is part of the Drift monorepo. For contributions, please see the main repository.
License
Dual-licensed under:
- AGPL-3.0 for open source projects
- Commercial License for proprietary use
See LICENSE.md for details.
For commercial licensing inquiries:
- Email: [email protected]
- Web: https://quarry-systems.com/license
