Architecture
The platform is organised in layers. Each layer depends only on the ones below it, and the core layer depends on nothing platform-specific. This is what keeps integrations decoupled from one another and from the runtimes that use them.
┌─────────────────────────────────────────────────────────┐│ Consumers ││ ┌───────────────┐ ┌───────────────┐ ┌──────────────┐ ││ │ Agent │ │ Sandbox │ │ HTTP API │ ││ │ (LLM tools) │ │ (scripts) │ │ (host) │ ││ └───────┬───────┘ └───────┬───────┘ └──────┬───────┘ ││ │ │ │ ││ └──────────────────┴─────────────────┘ ││ │ ││ Actions (typed) │├─────────────────────────────┼─────────────────────────────┤│ Providers │ ││ ┌──────────────────────────▼──────────────────────────┐ ││ │ ProviderInstance: actions · entities · events │ ││ │ (adapts one external system into the contracts) │ ││ └──────────────────────────┬──────────────────────────┘ │├─────────────────────────────┼─────────────────────────────┤│ Core contracts ▼ ││ actions · entities · events · items · registries ││ Services (DI) · database · buckets · errors · prompts │└─────────────────────────────────────────────────────────┘Core contracts
Section titled “Core contracts”The core layer (@grundlag/core) defines the vocabulary and nothing else. It has no
knowledge of any specific integration. It provides:
- The
define*builders —defineAction,defineProviderType,defineEntityType,defineEvent,defineItemType,defineDatabase— each of which pairs a definition with Zod schemas for validation. - The registries —
ActionRegistry,EntityTypeRegistry,EventRegistry,ProviderRegistry,ProviderTypeRegistry,ItemTypeRegistry— behaviour-oriented collections you register into, list, and look up by id. Services, a dependency-injection container for shared capabilities (see Services and dependency injection).- Two storage helpers for provider-local state, each declared with
defineDatabase/defineBucketand backed by a creator the host supplies: a database built on Kysely, and buckets — an S3-shaped object store for file blobs.
Because the core is only contracts, it is the one dependency every other package shares.
A second contract package, @grundlag/shared, sits alongside it and holds the pieces
that must be identical on both sides of an HTTP gap: the neutral Tool interface the sandbox
and agent consume, the conversation-item store, and the client/server wire protocol. The core
depends on it so a provider action can be bound into a tool; the client depends on it so a
remote caller speaks the same protocol. See Tools and transports.
Providers
Section titled “Providers”A provider adapts a single external system into the core contracts. It is packaged as a
ProviderType — a factory with a config schema and a setup function — that produces a
ProviderInstance at runtime. The instance owns four registries. Three of them — actions,
events, and entities — are the provider’s public surface: whatever it registers there is
everything the platform can see, while its own connection handling, caching, and API calls
stay private behind those contracts. The fourth, buckets, is declaration rather than
publication: it makes the provider’s blob storage known at boot, and a registered bucket
stays private unless it is marked exposed.
Providers depend on the core and nothing else. They never import the server or an agent. This is what lets the same provider run inside an HTTP host, an agent, and a sandbox without change. See Providers and registries.
The host
Section titled “The host”A host is an application you compose. It creates a Services container, registers the
provider types it wants with their configuration, and exposes the result. The
@grundlag/server package gives you createServer, a Fastify app that turns every
registered provider’s actions and entities into a typed HTTP API, plus an eval endpoint and
MCP. You write a small entry file that wires your providers into it — the platform ships
the building block, not a fixed deployment. See
Compose your own host.
Consumers
Section titled “Consumers”Consumers operate over actions without caring where they came from. A consumer can be
anything — an ordinary web application, a background script, an automation, or an
LLM-driven agent — because they all speak the same typed contracts. ProviderRegistry
flattens every registered provider’s actions into one namespaced map (via toActions()),
and each built-in consumer takes it from there:
- The agent (
@grundlag/agent) converts each action’s input schema into an OpenAI tool definition and runs the model’s tool-calling loop. See The agent run loop. - The sandbox (
@grundlag/sandbox) exposes each action as a typed async function to a script it executes in an isolated QuickJS runtime, recording a call log so a script can be resumed deterministically. See Determinism and resumption. - The HTTP API exposes each action as a
POSTendpoint with request and response validation derived from the schemas.
Across the HTTP gap: the client
Section titled “Across the HTTP gap: the client”The agent and the sandbox are not tied to the server process. Both consume a neutral Tool
— an action bound into a container-free callable — and a tool can be backed either by a direct
in-process call or by an HTTP request to a running host. @grundlag/client provides the
second binding: it reads a host’s manifest, generates a client typed against that host’s
providers, and reconstructs the host’s actions as HTTP-backed tools. Hand those to a sandbox or
agent and the run executes on the client while the capabilities stay behind the host. This is
what lets you build the same runner on either side of the network. See
Tools and transports.
The thread that ties it together: interrupts
Section titled “The thread that ties it together: interrupts”One mechanism spans all layers. An action can pause itself by throwing an ActionInterrupt
— to ask a human for approval, say. That interrupt propagates up through whichever consumer
is running: the sandbox captures it as a resumable checkpoint, and the agent surfaces it as
an interrupted run you can later resume or reject. Because the interrupt is defined in the
core, every layer understands it. See Interrupts.