Receipts for agent work.
Your agent runs code. Lab gives back a URL. The URL is the proof — readable by the next agent, verifiable by you.
LLMs do work, then describe what they did. The description is not the work. Lab runs the work in a sandbox and saves a canonical record at a URL: the agent that ran it can read its own receipt to retry or self-heal, the next agent can pick up from where the last one stopped, and a human can open the URL and see exactly what happened — without re-running anything.
One artifact. Two readers. Same source of truth.
Try it now: lab.coey.dev/compose
0.0.3 — API and result shapes may still move. Pin to exact versions or self-host.
npm install @acoyfellow/labimport { createLabClient } from "@acoyfellow/lab";
const lab = createLabClient({
baseUrl: process.env.LAB_URL, // or https://lab.coey.dev
});
// Run a chain, get a shareable result URL
const out = await lab.runChain([
{ name: "Load", body: `return { value: 42 }`, capabilities: [] },
{ name: "Double", body: `return { value: input.value * 2 }`, capabilities: [] },
]);
console.log(out.result); // { value: 84 }
console.log(out.resultId); // abc123
// Open the viewer: $LAB_URL/results/abc123
// Get the JSON: $LAB_URL/results/abc123.jsonWhat you'll see:
- Code — what ran in each step
- Capabilities — what the code could access
- Result — return values, timing, and any errors
Three things to know:
Every run saves canonical JSON at /results/:id.json, viewable at /results/:id. It includes the code, the inputs, the outputs, the timings, and any errors. The URL is the artifact.
A step fails → the receipt includes the error → the agent reads its own receipt → patches → retries. No external memory, no shared database. The agent's last failure is the input to its next attempt.
Agent A finishes, returns a resultId. Agent B opens /results/:id.json and continues from there. The receipt is the entire interface between them.
These are the workflows agents build with Lab:
Ship agent code with proof it works.
const out = await lab.runChain([
{ name: "Unit Tests", body: testCode, capabilities: [] },
{ name: "Integration", body: integrationCode, capabilities: ["kvRead"] },
]);
// Share the result URL → "10/10 tests passed"Auto-fix failures without human intervention.
const steps = [
{ name: "Parse", body: `try { return JSON.parse(input.raw) } catch(e) { return { error: e.message } }`, capabilities: [] },
{ name: "Heal", body: `if (!input.error) return input; const fixed = input.raw.replace(/,(\s*[}\]])/g, '$1'); return JSON.parse(fixed);`, capabilities: [] },
];Multi-agent relay — each step can spawn the next.
await lab.runChain([
{ name: "Planner", body: plannerCode, capabilities: ["workersAi"] },
{ name: "Coder", body: coderCode, capabilities: ["spawn"] },
{ name: "Reviewer", body: reviewerCode, capabilities: [] },
]);Compare old vs new logic before shipping.
const [old, neu] = await Promise.all([
lab.runSandbox({ body: oldLogic, capabilities: [] }),
lab.runSandbox({ body: newLogic, capabilities: [] }),
]);
// Compare outputs, then decideFind breaking points.
const runs = await Promise.all(
Array.from({ length: 50 }, () =>
lab.runSandbox({ body: targetCode, capabilities: [] })
)
);
// Check which runs failed and whySee full patterns: lab.coey.dev/docs/patterns
| Method | Path | Description |
|---|---|---|
GET |
/health |
Health check |
POST |
/run |
Run code sandbox |
POST |
/run/kv |
Run with kvRead capability |
POST |
/run/chain |
Multi-step workflow |
POST |
/run/spawn |
Nested isolates with depth budget |
POST |
/run/generate |
AI-generated code + run |
POST |
/seed |
Write demo KV data |
GET |
/lab/catalog |
Capability metadata for agents |
GET |
/results/:id |
Human viewer |
GET |
/results/:id.json |
Canonical result JSON |
import { createLabClient } from "@acoyfellow/lab";
const lab = createLabClient({ baseUrl: "..." });
lab.runSandbox({ body, capabilities? }) // Single sandbox
lab.runKv({ body }) // With KV snapshot
lab.runChain(steps) // Multi-step
lab.runSpawn({ body, capabilities?, depth? }) // Nested isolates
lab.runGenerate({ prompt, capabilities? }) // AI-generated code
lab.seed() // Seed demo data
lab.getResult(resultId) // Fetch saved result JSONEffect client: import { createLabEffectClient } from "@acoyfellow/lab/effect"
Each step only gets what you explicitly grant:
| Capability | What the guest gets |
|---|---|
kvRead |
Read-only KV: kv.get(key), kv.list(prefix) |
workersAi |
ai.run(prompt) — keys stay on host |
r2Read |
r2.list(), r2.getText(key) |
d1Read |
d1.query(sql) — read-only queries |
spawn |
spawn(code, caps) — nested child isolates |
durableObjectFetch |
labDo.fetch(name, { method, path, body }) |
containerHttp |
labContainer.get(path) — bound container service |
No capabilities = pure compute, no I/O. Denied capabilities produce clear errors in the saved result.
Give agents access to Lab via MCP:
npm install -g @acoyfellow/lab-mcp{
"mcpServers": {
"lab": {
"command": "npx",
"args": ["-y", "@acoyfellow/lab-mcp"],
"env": { "LAB_URL": "https://your-lab.example" }
}
}
}Tools: find (discover capabilities, fetch results) and execute (run any mode).
Your agents, your data, your capabilities:
git clone https://github.com/acoyfellow/lab.git && cd lab
bun install && bun run deployRequires Cloudflare Workers Paid ($5/mo). Provisions the public app, private Worker, auth D1, engine D1, KV, Worker Loader, Durable Objects, and optional R2/AI bindings via Alchemy.
worker/ Sandbox engine (Effect v4, Worker Loaders)
index.ts Routes, chain/spawn orchestration, result storage
Loader.ts V8 sandbox lifecycle
guest/templates.ts Guest module composition + capability shims
capabilities/ Capability registry
packages/
lab/ TypeScript client (@acoyfellow/lab)
lab-mcp/ MCP server (@acoyfellow/lab-mcp)
lab-cli/ CLI tools
lab-petri/ Runtime utilities
src/ SvelteKit app (compose, viewer, docs)
alchemy.run.ts Infrastructure-as-code
bun dev # Worker (port 1337) + SvelteKit app
bun test # Guest body syntax validation
bun run lint # oxlint
bun run check # svelte-check + typecheckIntegration tests in worker/index.test.ts run against a live Worker.
MIT