Embedding the engine
Host Marbles in a Bun application with explicit dependencies and lifecycle ownership.
Embed Engine from @foundry.sh/marbles/lib when a Bun application needs to
launch and inspect Marbles work. The host supplies configured package
instances, registers definitions, and owns startup and shutdown.
Marbles connects those instances to step contexts, the Feed, approvals, and run execution. The package map explains what each library provides.
Create and run an engine
This example counts files with in-memory stores and no agent provider or sandbox runtime:
import { step } from "@foundry.sh/marbles";
import {
ArtifactManager,
Engine,
InMemoryArtifactStore,
InMemorySessionStore,
ModelManager,
} from "@foundry.sh/marbles/lib";
const engine = new Engine({
models: new ModelManager(),
sessions: new InMemorySessionStore(),
// Called at the first sandbox; `createContainers` over your runtime.
containers: () => Promise.reject(new Error("no sandbox runtime here")),
artifacts: new ArtifactManager({ store: new InMemoryArtifactStore() }),
root: process.cwd(),
workspaceId: "my-app",
// Add a state directory to persist run records. Configure persistent
// session and artifact stores separately; these two stores are in memory.
askable: false, // true when something in this process can answer `ask`
});
engine.define(
step("inventory").do(
async ({ workspaces }) => (await workspaces.current.files()).length
)
);
try {
await engine.start();
const count = await engine.run<number>("inventory", {});
console.log({ count });
} finally {
await engine.stop();
await engine.dispose();
}
models, sessions, containers, and artifacts are required. workspaces
is optional: without it the engine builds a workspace system over the local
filesystem, watched, whose git() layer runs git as the git option says. A
host that brings its own WorkspaceSystem builds its git() layer with the
same options it passes as git. The engine hands the instances back
(engine.models, engine.sessions, engine.workspaces.system,
engine.sandboxes.containers()) and closes them in dispose().
engine.store is the state store it keeps trigger state in, and
engine.scopes is a read-only view of the runs in flight: each run’s
RunScope by run id.
Launch without waiting
For background work in a long-lived host, launch returns the run ID and a
result promise after dispatch, rather than waiting for execution to finish:
const launched = await engine.launch<number>("inventory", {});
console.log({ runId: launched.id });
const count = await launched.result;
run(name, input) and execute(definition, input) wait for the result
instead. Register a definition with define to launch it by name. The
MCP tools use launch for workflows and one
registered agent-dispatch step, exposing the handle’s id as runId for both.
The host must keep the engine alive while work executes, handle a rejected result promise, and stop/dispose the engine on shutdown. A persisted engine writes the run output journal while work runs; that output is separate from the run’s recovery snapshot.
Use resources directly
This excerpt assumes the engine above and configured model and
container providers. The managers a step body gets as agents, workspaces, sandboxes, and
artifacts are on the engine too, with the same methods, and work without a
run and before start():
import { agent, artifact } from "@foundry.sh/marbles";
const reviewer = agent({ prompt: "Review the diff." });
const report = artifact({ name: "review", type: "text/markdown" });
const repo = await engine.workspaces.load({ path: "/path/to/repo" });
const session = await engine.agents.session(reviewer, { cwd: repo.root });
const reply = await session.generate("Review this.");
const box = await engine.sandboxes.start({ image: "oven/bun:1-slim" });
const version = await engine.artifacts.write(report, {
"report.md": reply.text,
});
Inside a step these calls are tied to the step: they replay to the same
session, sandbox, or version, stop when the step is cancelled, and close when
the run settles. On the engine there is no step, so each call is new, a
sandbox is yours to close, and whatever is still open closes on dispose().
engine.sandboxes.containerOf(box) returns the container behind a sandbox
the engine opened.
Register work
The engine owns its registry:
engine.define(definition)adds a named step or workflow. The authoring words return exactly that, soengine.define(step("x").do(...))works.engine.schedule(record)registers a schedule; it does not start a clock.engine.monitor({ key, source, trigger, handler })registers a monitor for a host to drive.engine.definitions(),engine.schedules(), andengine.monitors()read it back, including the triggers agents created.
Importing a .foundry/marbles/inspect.ts does not fill an engine you built. The module’s
own collection is private to the CLI, which copies it into the engine it
builds. A host that wants a module’s definitions exports them from the module
and defines them.
Host settings
| Option | Responsibility |
|---|---|
print |
Receives engine status and body log lines. |
home |
Selects the directory for shared skills. |
git |
Chooses the Git runner, such as one that echoes commands. |
worktrees |
Sets the default checkout directory, otherwise Engine.worktreesFor(state). |
dry |
Runs sandbox agent requests in the host process. |
store |
Provides trigger history, observations, agent-created triggers, and their locks. |
store defaults to JsonStateStore when state is set, or
InMemoryStateStore otherwise. Both adapters are exported.
Persistence and durability
Workflows provides the orchestrator and step execution. Marbles adds run-owned sessions, resources, and the saved ledger used when a parked run replays. The durability contract still applies when you embed the engine; the library’s broader recovery APIs do not change Marbles’ adoption rules.
The example above deliberately keeps everything in memory. Supplying
state enables run persistence and the default persistent trigger store;
provide a persistent session store and artifact store with durable bytes
separately. Persisting run records alone cannot retain agent transcripts
or report files. Set askable: true only when the host can present and
answer questions. Define the same named work before start() so parked
runs can be adopted.
Package instances
The package bundles its Foundry packages, so take the classes the
instances are built from out of @foundry.sh/marbles/lib as well. An instance
built from another copy of the same class is a different type. Besides
Engine, its types, and three constants (CLI_HARNESS_IDS, CLI_HARNESSES,
TERMINAL_RUN_STATUSES), the entry exports only those:
| Instance | Exports | Underlying library |
|---|---|---|
models |
ModelManager, claudeCodeProvider, codexProvider |
Models |
sessions |
InMemorySessionStore, JsonSessionStore |
Agents; JSON adapter in Marbles. |
workspaces |
WorkspaceSystem, directory, git, nodeObserver |
Workspaces |
containers |
createContainers, createMemoryContainerStore |
Sandbox |
artifacts |
ArtifactManager, InMemoryArtifactStore, JsonArtifactStore, blobFiles |
Artifacts |
store |
JsonStateStore, InMemoryStateStore |
Marbles’ trigger state and local locks. |
Two things a host cannot do through this entry yet: the MicroSandbox runtime
createContainers needs is not exported, and nothing fires the triggers the
engine holds (the CLI’s schedule loop is not part of the engine).