Skip to content
Foundry
Esc
↑↓navigate↵open⌘Jpreview
On this page

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, so engine.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(), and engine.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).

Was this page helpful?