@sediment/sediment
v0.2.5
Published
TypeScript framework for building composable AI systems
Readme
Sediment
A TypeScript framework for building composable AI systems. It handles the boring parts (logging, retries, concurrency, caching) so you can focus on the interesting parts (prompts, workflows, tools).
Why Sediment?
Sediment is built around four core values that make AI systems sustainable over time:
Composability
Avoid the "goto" anti-pattern. Monolithic agents where "anything can call anything" are like writing programs with jumps and gotos—the allure of generalization produces pipelines that are hard to understand and steer.
Instead, Sediment encourages higher-level tasks that orchestrate lower-level tasks rather than calling tools directly. This provides:
- High parallelism: After planning, many subtasks can fire off concurrently
- Clean context: Lower-level tasks filter and process results before returning to orchestrators
- Steerability without sacrificing autonomy: A higher-level model steers at a high level of abstraction rather than contending with individual tool details
Observability
Design systems to be observable, explainable, and debuggable. You should be able to root cause and reproduce failures from single production examples.
Sediment can log the entire payload to/from inference servers when you opt into full logging. By default, sensitive fields are redacted. Each log record includes:
- Metadata (which step, identifiers)
- Complete request payload
- Complete response stream (all tokens when
logChunks: true)
For 400+ step pipelines, you can replay every completion call and identify exactly where non-determinism caused a failure.
Statelessness
The core pipeline is a pure function: input → output.
Actions as Nouns, Not Verbs: Actions that the model wants to take are not verbs (executed immediately). They're nouns (data structures describing intended effects). The caller validates and executes effects after the pipeline completes.
When actions are nouns, you get powerful behavior for free:
- Models can review, rank, and correct actions (they're just JSON payloads)
- You can generate draft candidates of many possible actions and vote across them
- Persistence for human review becomes trivial
- Eval environments are simpler—you examine drafted actions rather than final world state
Changeability
Maintain disciplined interface boundaries between layers that change at different rates:
- Core APIs (slowest changing) - Step API, adapters, log types. Like the stud system that makes Lego blocks fit together.
- Lower-level tasks (medium) - Entity resolution, answer generation, context gathering. The building blocks themselves.
- Product surfaces (fastest) - Interactive research, background jobs, specific workflows. Full Lego sets assembled from building blocks.
This separation means vendor API changes are insulated from the rest of the system, and new UI features can be added without affecting your building blocks.
Installation
Sediment is published as the public scoped npm package @sediment/sediment.
Local Development
Install with your package manager of choice:
pnpm add @sediment/sedimentImport the eval helpers from the eval subpath:
import { createEvalRunner } from '@sediment/sediment/eval';GitHub Actions (Consumer Repos)
Use the ready template:
docs/examples/consumer-github-actions.yml
It installs @sediment/sediment from public npm without a Sediment-specific npm token. Add npm auth only if the consumer repo also depends on other private packages.
Vercel and Cloudflare
For deploy platforms that install dependencies during builds, no NPM_TOKEN is required for @sediment/sediment. Add npm auth only for other private dependencies.
Publishing from this Repo
Do the first publish manually from this repo after npm login.
After that, configure npm trusted publishing for this repo and future releases can come from tags. Once trusted publishing is configured, pushing a vX.Y.Z tag makes GitHub Actions:
- validate the package
- pack the exact tarball consumers install
- publish
@sediment/sedimentto npm - attach the tarball to a GitHub Release
The detailed npm setup, first publish steps, and consumer install patterns live in docs/platform-distribution.md.
Quick Start
import { Runtime, createAnthropicAdapter, defineTask } from '@sediment/sediment';
// Create a runtime with your API key
const runtime = Runtime.create({
completions: createAnthropicAdapter({
apiKey: process.env.ANTHROPIC_API_KEY
}),
});
// Define a task
const summarize = defineTask<string, string>(
'summarize',
async function* (text, deps) {
for await (const chunk of deps.completions.complete({
model: 'claude-sonnet-4-5-20250929',
messages: [
{ role: 'user', content: `Summarize this: ${text}` }
],
maxTokens: 200,
})) {
if (chunk.type === 'token') {
yield chunk.content;
}
}
}
);
// Run it with ExecutionResult
const result = await runtime.runTask(summarize, 'Your text here...');
console.log(result.outputs.join(''));
console.log(`Cost: $${result.estimatedCost.toFixed(4)}`);Core Concepts
Tasks
Tasks are typed functions that call models and tools. They stream outputs and don't execute side effects directly.
const myTask = defineTask<Input, Output>('name', async function* (input, deps) {
// Use deps.completions for model calls
// Use deps.tools for tool execution
// Use deps.effects to collect side effects
// Use deps.citations to track sources
yield output;
});Runtime
The runtime provides dependencies to tasks. It handles logging, concurrency, and caching automatically.
const runtime = Runtime.create({
completions: createAnthropicAdapter({ apiKey }), // or createOpenAIAdapter
logger: createConsoleLogger(), // optional
pricing: MY_PRICING_TABLE, // optional, for cost tracking
logChunks: true, // enables replay capability
});ExecutionResult
When you run a task through the Runtime, you get back an ExecutionResult that bundles outputs with all collected stores and metadata:
const result = await runtime.runTask(myTask, input);
// All outputs yielded by the task
console.log(result.outputs);
// Context ID for correlating with logs
console.log(result.contextId);
// Access collected effects (side effects as data)
const effects = result.effectStore.getAll();
for (const effect of effects) {
if (effect.type === 'send_email') {
await sendEmail(effect.payload); // Execute after validation
}
}
// Access citations for attribution
const citations = result.citationStore.getAll();
// Estimated cost in USD
console.log(`Cost: $${result.estimatedCost.toFixed(4)}`);You can also use Runtime.createReplayable() to create a runtime with full replay capability enabled:
// Enables logChunks: true with unlimited capture
const runtime = Runtime.createReplayable({
completions: adapter,
pricing: MY_PRICING,
});Effects
Instead of executing side effects (sending emails, updating databases), tasks collect them as data. You decide when to execute.
deps.effects.add({
type: 'send_email',
payload: { to: '[email protected]', subject: 'Hello', body: '...' }
});
// Later, inspect and execute
const effects = deps.effects.getAll();Citations
Track where information came from:
deps.citations.add({
id: 'cite_1',
source: 'https://example.com',
content: 'The relevant quote...',
});Step DSL
Sediment provides a fluent DSL for building typed pipelines declaratively:
import { sediment, sequence, parallel } from '@sediment/sediment';
// Basic pipeline
const pipeline = sediment
.input<string>()
.completion('analyze', {
model: 'claude-sonnet-4-5-20250929',
renderPrompt: (q) => [{ role: 'user', content: q }],
})
.parse({ type: 'json' })
.toTask('analysis_pipeline');Step Methods
| Method | Description |
|--------|-------------|
| .completion(name, config) | Call a model with the given config |
| .parse(schema) | Parse output (json, capture, regex, lines) |
| .map(fn) | Transform each output |
| .filter(predicate) | Filter outputs |
| .collect() | Collect all outputs into an array |
| .branch(branches) | Run multiple steps in parallel on the same input |
| .loop(condition, body, maxIterations) | Loop while condition is true |
| .conditional(predicate, ifTrue, ifFalse) | Conditional branching |
| .effect(type, payloadFn) | Add an effect to the effect store |
| .dict(schemas) | Apply multiple parse schemas to the same output |
| .debug(label, options) | Log step execution for debugging |
| .tap(fn) | Side effect without transforming output |
| .take(n) | Take only first N outputs |
| .tool(name, mapper) | Execute a tool |
| .toTask(name) | Convert Step to a named Task for use with Runtime |
| .merge(combiner) | Combine results from branch operations |
Parse Schemas
// JSON parsing
.parse({ type: 'json' })
// Capture between delimiters
.parse({ type: 'capture', start: '```json', end: '```' })
// Regex extraction
.parse({ type: 'regex', pattern: /\d+/ })
// Split into lines
.parse({ type: 'lines', filter: line => line.trim().length > 0 })Dict: Multiple Parse Schemas
Extract multiple values from the same output:
const step = sediment.input<string>()
.completion('generate', config)
.dict({
urls: sediment.capture('```urls', '```').collect(),
search: sediment.capture('```search', '```').collect(),
});
// Returns { urls: string, search: string }Debug: Observability Integration
Log step execution while passing output through unchanged:
const step = sediment.input<string>()
.completion('analyze', config)
.debug('analyze_step', { rawChunks: true, truncateLength: 1000 });Global Combinators
// Sequential composition of Tasks
const pipeline = pipe(task1, task2, task3);
// Parallel execution of Tasks on same input
const fanOut = parallel(searchWebTask, searchInternalTask, searchCacheTask);Note: The
pipeandparallelcombinators work with Tasks (created viadefineTaskor.toTask()). For composing Steps before converting to a Task, use the fluent.branch()method, or usesequence/parallelStepsfrom the Step module.
Cost Tracking
Sediment tracks token usage and calculates cost estimates. You provide a pricing table with your models:
import type { PricingTable } from '@sediment/sediment';
const MY_PRICING: PricingTable = {
'claude-sonnet-4-5': { inputTokenCostPer1m: 3, outputTokenCostPer1m: 15 },
'claude-haiku-4-5': { inputTokenCostPer1m: 1, outputTokenCostPer1m: 5 },
'gpt-4o': { inputTokenCostPer1m: 2.5, outputTokenCostPer1m: 10 },
};
const runtime = Runtime.create({
completions: adapter,
pricing: MY_PRICING,
});
const result = await runtime.runTask(myTask, input);
console.log(`Estimated cost: $${result.estimatedCost.toFixed(4)}`);Pricing lookup supports prefix matching for versioned models—claude-sonnet-4-5-20250929 matches the claude-sonnet-4-5 entry.
Debugging Workflow
Sediment enables a tight debugging loop from production failures:
1. Capture Request ID
Every runtime instance has a request ID that links all traces:
const runtime = Runtime.create({ completions: adapter });
const requestId = runtime.getRequestId(); // Pass to UI for feedback capture2. Query Logs
const logger = runtime.getLogger();
const records = logger.getRecords();
// Filter by context path for parallel execution traces
const branch0 = records.filter(r => r.contextPath?.startsWith('root/parallel[0]'));
// Each record contains:
// - params: the CompletionParams sent
// - response: { content, toolCalls, usage, chunks (if logChunks enabled) }
// - durationMs, timestamp, contextPath, cost3. Replay Locally
With logChunks: true, you can replay the exact sequence of chunks:
const runtime = Runtime.createReplayable({
completions: createCachedAdapter(
createAnthropicAdapter({ apiKey }),
{ storage: createFileCacheStorage('.cache/completions') }
),
});4. Fix and Add to Eval Suite
// Same task definitions, same runtime with caching
await runner.runAll([
{
name: 'regression_case_123',
input: capturedInput,
assertions: [{ type: 'contains', value: 'expected' }],
},
]);Building on Top
Sediment has four layers:
| Layer | What It Is | Examples | |-------|------------|----------| | 0 | Platform primitives | Model completions, tool execution (this package) | | 1 | Building blocks | Entity resolution, search, answer generation | | 2 | Composite tasks | Research loops, multi-step workflows | | 3 | Products | Chat assistants, alerting systems, report generators |
You build upward. Define Layer 1 tasks, compose them into Layer 2, wire Layer 2 into products.
Example: Building Block Task
// Layer 1: A reusable search task
const searchWeb = defineTask<string, SearchResult[]>(
'search_web',
async function* (query, deps) {
const results = await deps.tools.execute('web_search', { query });
// Register citations for each result
for (const result of results) {
deps.citations.add({
id: `web_${result.id}`,
source: result.url,
content: result.snippet,
});
}
yield results;
}
);Example: Composite Task
// Layer 2: Compose building blocks
const researchQuestion = defineTask<string, ResearchResult>(
'research_question',
async function* (question, deps) {
// Run searches in parallel
const [webResults, internalResults] = await Promise.all([
runTask(searchWeb, question, deps),
runTask(searchInternal, question, deps),
]);
// Synthesize an answer
const answer = await runTask(generateAnswer, {
question,
context: [...webResults, ...internalResults],
}, deps);
yield answer;
}
);Context Paths (Trace Hierarchy)
When running parallel tasks, Sediment tracks the execution hierarchy for visualization:
import { parallel, pipe } from '@sediment/sediment';
// Nested parallel execution using Task combinators
// (These are Tasks, not Steps - use .toTask() to convert Steps first)
const pipeline = pipe(
taskA,
parallel(
pipe(taskB1, taskB2),
pipe(taskC1, taskC2),
),
taskD
);
// Each completion/tool record includes contextPath:
// - taskA: "root"
// - taskB1, taskB2: "root/parallel[0]"
// - taskC1, taskC2: "root/parallel[1]"
// - taskD: "root"Adapter Reference
This is the menu of what's available. Build tasks by combining these capabilities.
Completions Adapters
All completions adapters share the same interface. Pass CompletionParams, get back streaming CompletionChunks.
CompletionParams
{
model: string; // Required: model identifier
messages: Message[]; // Required: conversation history
temperature?: number; // 0.0 - 1.0, controls randomness
maxTokens?: number; // Max tokens to generate
tools?: ToolDefinition[]; // Tools the model can call
toolChoice?: 'auto' | 'none' | 'required' | { type: 'function'; function: { name: string } }; // How the model should use tools
stopSequences?: string[]; // Custom stop sequences
adapterExtras?: AdapterExtras; // Vendor-specific parameters
}CompletionChunks (What You Get Back)
// Text content streaming
{ type: 'token'; content: string }
// Model wants to call a tool
{ type: 'tool_call'; toolCall: { id: string; name: string; arguments: string } }
// Extended thinking content (Anthropic only)
{ type: 'thinking'; thinking: string; signature?: string }
// Reasoning summary content (provider-neutral, not raw chain-of-thought)
{ type: 'reasoning_summary'; content: string; done?: boolean }
// Stream complete
{ type: 'done'; usage?: { inputTokens: number; outputTokens: number } }Anthropic Adapter
import { createAnthropicAdapter } from '@sediment/sediment';
const adapter = createAnthropicAdapter({
apiKey: string; // Required
baseUrl?: string; // Default: https://api.anthropic.com/v1/messages
maxConcurrency?: number; // Default: 5
maxRetries?: number; // Default: 3
timeoutMs?: number; // Default: 60000
});This adapter uses the official Anthropic SDK (@anthropic-ai/sdk) with typed streaming and automatic retry handling.
Supported Models: claude-sonnet-4-5-20250929, claude-opus-4-5-20251101, claude-haiku-4-5-20251001, etc.
Unique Features:
- Extended Thinking: Claude can show its reasoning process before answering.
const chunks = deps.completions.complete({
model: 'claude-sonnet-4-5-20250929',
messages: [{ role: 'user', content: 'Solve this step by step...' }],
maxTokens: 16000,
adapterExtras: {
anthropic: { thinking: { type: 'enabled', budgetTokens: 8000 } }
},
});
for await (const chunk of chunks) {
if (chunk.type === 'thinking') {
console.log('[Thinking]', chunk.thinking);
}
if (chunk.type === 'token') {
process.stdout.write(chunk.content);
}
}OpenAI Adapter
import { createOpenAIAdapter } from '@sediment/sediment';
const adapter = createOpenAIAdapter({
apiKey: string; // Required
baseUrl?: string; // Default: https://api.openai.com/v1
maxConcurrency?: number; // Default: 5
maxRetries?: number; // Default: 3
timeoutMs?: number; // Default: 60000
});This adapter uses the official OpenAI SDK (openai) and the Responses API streaming interface.
OpenAI reasoning summaries are opt-in through adapterExtras.openai.reasoningSummary. The adapter streams them as provider-neutral reasoning_summary chunks and does not expose raw chain-of-thought.
for await (const chunk of deps.completions.complete({
model: 'gpt-5.4',
messages: [{ role: 'user', content: 'Plan and answer.' }],
adapterExtras: {
openai: { reasoningEffort: 'low', reasoningSummary: 'auto' },
},
})) {
if (chunk.type === 'reasoning_summary') {
updateWorkingState(chunk.content, { done: chunk.done });
}
}Cached Adapter
Wraps any completions adapter to add caching. Essential for evals (deterministic replay) and development (avoid repeated API calls).
import { createCachedAdapter, createInMemoryCacheStorage, createFileCacheStorage } from '@sediment/sediment';
// In-memory cache (for tests)
const cached = createCachedAdapter(
createAnthropicAdapter({ apiKey }),
{ storage: createInMemoryCacheStorage() }
);
// File-based cache (for development/evals)
const cached = createCachedAdapter(
createAnthropicAdapter({ apiKey }),
{ storage: createFileCacheStorage('.cache/completions') }
);Configuration:
{
storage: CacheStorage; // Required: where to store cached completions
keyFn?: (params) => string; // Custom cache key function
excludeFields?: string[]; // Fields to exclude from cache key
}Storage Implementations:
| Storage | Use Case | Persistence |
|---------|----------|-------------|
| createInMemoryCacheStorage() | Unit tests | None (in-process only) |
| createFileCacheStorage(dir) | Development, evals | Filesystem |
Tools Adapter
Register and execute tools that models can call.
import { createToolsAdapter } from '@sediment/sediment';
const tools = createToolsAdapter();
// Register a tool
tools.register(
{
name: 'get_weather',
description: 'Get current weather for a city',
parameters: {
type: 'object',
properties: {
city: { type: 'string', description: 'City name' },
units: { type: 'string', enum: ['celsius', 'fahrenheit'] },
},
required: ['city'],
},
},
async (args) => {
// Your implementation
return JSON.stringify({ temp: 72, conditions: 'sunny' });
}
);
// Pass to runtime
const runtime = Runtime.create({
completions: createAnthropicAdapter({ apiKey }),
tools,
});
// Tools are automatically available to models
const deps = runtime.getDeps();
for await (const chunk of deps.completions.complete({
model: 'claude-sonnet-4-5-20250929',
messages: [{ role: 'user', content: 'What is the weather in NYC?' }],
tools: deps.tools.list(), // Pass registered tools
toolChoice: 'auto',
})) {
if (chunk.type === 'tool_call') {
// Execute the tool
const result = await deps.tools.execute(
chunk.toolCall.name,
JSON.parse(chunk.toolCall.arguments)
);
console.log('Tool result:', result);
}
}ToolsAdapter Methods:
| Method | Description |
|--------|-------------|
| register(definition, handler) | Register a tool with its handler function |
| execute(name, args) | Execute a tool, returns ToolResult |
| list() | Get all registered tool definitions |
| has(name) | Check if a tool is registered |
Stores
EffectStore
Collect side effects as data instead of executing them immediately.
deps.effects.add({ type: 'send_email', payload: { to, subject, body } });
deps.effects.add({ type: 'update_db', payload: { table, record } });
// Later, review and execute
const effects = deps.effects.getAll();
for (const effect of effects) {
if (effect.type === 'send_email') {
await sendEmail(effect.payload);
}
}CitationStore
Track information sources for attribution.
// Add a citation with explicit id
deps.citations.add({
id: 'cite_1',
source: 'https://example.com/article',
content: 'The relevant quote...',
});
// Or use the create() helper to auto-generate an id
const citationId = deps.citations.create(
'https://example.com/article',
'The relevant quote...'
);
// Retrieve citations
const fetched = deps.citations.get('cite_1');
const allCitations = deps.citations.getAll();LogStore
Full request/response logging with context path tracking.
const logger = runtime.getLogger();
// Get all completion records
const records = logger.getRecords();
// Filter by context path (for parallel execution traces)
const branch0 = records.filter(r => r.contextPath?.startsWith('root/parallel[0]'));
// Each record contains:
// - params: the CompletionParams sent
// - response: { content, toolCalls, usage }
// - durationMs, timestamp, contextPathEval
Test runner with deterministic caching for reproducible evaluations.
import { createEvalRunner } from '@sediment/sediment/eval';
import { createFileCacheStorage, createCachedAdapter, createAnthropicAdapter } from '@sediment/sediment';
const runner = createEvalRunner(summarizeTask, {
runtimeConfig: {
completions: createCachedAdapter(
createAnthropicAdapter({ apiKey }),
{ storage: createFileCacheStorage('.cache/eval') }
),
},
});
// Define test cases
await runner.runAll([
{
id: 'summarization_quality',
input: 'Long article text...',
assertions: [
{ type: 'contains', value: 'key point' },
{ type: 'token_count', max: 200, countType: 'output' },
],
},
]);Versioning and Upgrades
- SemVer is enforced via git tags, npm releases, and GitHub Releases.
- Breaking changes ship only in major versions and include migration notes.
- New features ship in minor versions.
- Fixes and internal hardening ship in patch versions.
Deprecation Policy
- Deprecated APIs remain available for at least one major cycle.
- Use CI dependency update PRs (Renovate/Dependabot) for controlled upgrades.
Docs
docs/platform-distribution.md- public npm package setup, publishing, and consumption.docs/security-controls.md- enforced and manual security controls.docs/polymer_spec.md- framework architecture and design goals.
