Run agents and sandboxes on the client
The typed client calls actions one at a time.
This guide goes further: it turns a host’s actions into a set of neutral tools and hands
them to a Sandbox or an agent — so the runner itself lives on the client, while the tools
it calls execute on the server. Because a sandbox and an agent consume the same
Tool shape whether the tools are local or remote, the run
looks identical to one hosted on the server; only the transport underneath differs.
Build HTTP-backed tools
Section titled “Build HTTP-backed tools”client.toTools(manifest) reconstructs the host’s provider actions as tools backed by HTTP
calls. Pass it the apiManifest from your
generated file — it carries the JSON Schemas the
tools rebuild their validation from, and the function names the server keyed them under:
import { createClient } from '@grundlag/client';import { apiManifest, type ApiSchema } from './client.generated.js';
const client = createClient<ApiSchema>({ baseUrl: 'http://localhost:3800' });const tools = client.toTools(apiManifest);// tools: Record<string, Tool> — e.g. tools.demoAdd, tools.demoEchoEach tool is keyed by the same function name the server assigns, so a script that calls
demoAdd(...) addresses the same action whether the sandbox is backed by in-process tools or
these HTTP-backed ones.
Run a sandbox on the client
Section titled “Run a sandbox on the client”Hand the tools to a Sandbox and evaluate a script. The add below runs on the server; only
its result crosses back:
import { Sandbox } from '@grundlag/sandbox';
const sandbox = new Sandbox({ actions: tools });
const { data } = await sandbox.eval({ script: 'const r = await demoAdd({ a: 2, b: 3 }); data.sum = r.sum;', data: {},});// data.sum === 5 — produced by the live server executing `add`The sandbox is running in your process; the tool call inside the script is the only thing that touches the network. Everything else about the sandbox — the deterministic call log, resumable interrupts, type-checked scripts — works unchanged. See Execute a script.
Wrap the sandbox as a tool
Section titled “Wrap the sandbox as a tool”The client tools compose with the rest of the toolkit. You can wrap a client-backed sandbox as a single tool — the same sandbox-as-a-tool pattern — so an agent orchestrates HTTP-backed actions by writing one script:
import { Sandbox, createSandboxTool } from '@grundlag/sandbox';import { createItemStore } from '@grundlag/shared';
const sandbox = new Sandbox({ actions: client.toTools(apiManifest) });const runScript = await createSandboxTool(sandbox);
const store = createItemStore();const output = await runScript.execute( { script: "data.echoed = (await demoEcho({ message: 'hi' })).message;" }, { logger, items: store.items },);Items and interrupts still work
Section titled “Items and interrupts still work”Two things a tool needs beyond its input still flow across the gap:
- Items. A tool reads and writes conversation items through its context. When you supply an
item store, the client sends its
current items with each call so the server’s
latestlookups resolve to the same values they would in-process, and folds back whatever the call emitted. Create one withcreateItemStoreand passstore.itemson the context. - Interrupts. An action that pauses server-side comes back as an envelope and is re-thrown
as a
ToolInterruptin your process — so a client-driven run suspends and resumes exactly like a server-driven one. See Interrupts.
import { createItemStore, type Logger } from '@grundlag/shared';
const store = createItemStore([{ itemType: 'memo', data: { text: 'remembered' } }]);const readMemo = tools.demoReadMemo;
// The action reads its `memo` item server-side; the value is there only because// the client forwarded the item snapshot over HTTP.const result = await readMemo.execute({}, { logger, items: store.items });When to use this
Section titled “When to use this”Reach for client-side tools when the runner should live outside the server — an agent loop
in a worker, a sandbox in a CLI, a script orchestrating a remote host — while the capabilities
stay behind the host. When the runner already lives inside the host, use the server’s own
ProviderRegistry.toTools() instead and skip the network entirely; the consuming code is the
same either way.
Next steps
Section titled “Next steps”- Tools and transports — the seam that makes this work.
- Wrap a sandbox as an agent tool — the pattern used above, in depth.
- Configure and run an agent — drive a full agent loop over these tools.