Pulse has a tiny core and a large ecosystem of plugins. This document defines the core concepts, the package boundaries, and the rules that keep them small.
The entire runtime is built from six primitives. If a seventh wants in, it must delete one first.
Runtime the entry point; owns a run
└─ Context what a run receives: signal, emit, tools, state
└─ Scheduler decides when/how work executes (order, concurrency)
└─ EventBus every step emits start/end events through here
└─ Registry where plugins, tools, and models are looked up
└─ Executor runs one Runnable and returns a Result
Everything else — Agent, Workflow, Memory, RAG, Planner — is a Runnable produced by a plugin and executed by the Executor. None of them are core concepts.
interface Runnable<In = unknown, Out = unknown> {
run(ctx: Context, input: In): Promise<Out>;
}agent(), workflow(), sequence(), tool() — all return a Runnable. This is why they compose without glue code.
A run receives exactly one object. It grows only when a capability truly needs it.
interface Context {
signal: AbortSignal; // cancellation
emit: (event: Event) => void; // events
tools: ToolRegistry; // available tools
state: StateStore; // scoped key/value for this run
log: Logger;
}Cancellation, retries, timeouts, and streaming are all expressed through this one object — not through new top-level APIs.
core ──────────────► (nothing)
runtime ─────────────► core
events ──────────────► core
tool ────────────────► core
model ───────────────► core (interfaces only, no providers)
memory ──────────────► core (interfaces only, no stores)
workflow ────────────► core, runtime
agent ───────────────► core, model, tool, memory (never a provider)
rag ─────────────────► core, model, memory
plugin ──────────────► core
@pulse/openai ───────► model (a plugin, ships separately)
@pulse/anthropic ────► model
@pulse/redis ────────► memory
@pulse/postgres ─────► memory
Rules:
corenever imports another Pulse package.agentimports themodelinterface, never@pulse/openai. The provider is injected at the call site. This is the dependency inversion that lets any provider drop in.- Providers and stores are always separate packages loaded with
runtime.use(...). Nothing is built in.
agent ──uses──► ChatModel (interface, in @pulse/model)
▲
│ implements
@pulse/openai
@pulse/anthropic
The agent does not know OpenAI exists. It knows ChatModel.
A plugin is a function that registers capabilities on a runtime.
interface Plugin {
name: string;
setup(registry: Registry): void;
}
runtime.use(openai()); // registers a ChatModel
runtime.use(redis()); // registers a MemoryStoreProviders, stores, tracers, and integrations are all plugins. The core ships none of them.
Memory is interfaces first, implementations later (as plugins).
interface MemoryStore { get(k): Promise<V>; set(k, v): Promise<void> }
interface VectorStore { query(embedding, k): Promise<Match[]> }
interface CheckpointStore { save(id, state): Promise<void>; load(id): Promise<State> }@pulse/redis, @pulse/sqlite, @pulse/postgres implement these. Core stays clean.
- New provider? New package. Core untouched.
- New execution shape (e.g.
race)? NewRunnableinworkflow. Core untouched. - New observability tool? Subscribe to the EventBus. No API change.
Core almost never changes. That's the design goal.