Workspace (Filesystem, Shell & Skills)
A workspace gives an agent a portable filesystem it can explore and edit — plus, optionally, a shell to run commands. It’s the substrate that skills, Open Knowledge Format bundles, and a durable scratch area all mount on. The filesystem is a single interface with swappable backends, so the same agent code runs in-memory for tests, on disk for Node, or on Durable-Object SQLite for Cloudflare.
npm install @kuralle-agents/fs @kuralle-agents/coreQuick start
Section titled “Quick start”Attach a FileSystem to AgentConfig.workspace. The runtime auto-registers a durable workspace
tool (read-only by default) so the model can ls, read, and grep the files.
import { defineAgent, createRuntime } from '@kuralle-agents/core';import { InMemoryFs } from '@kuralle-agents/fs';
const fs = new InMemoryFs({ '/kb/hours.md': '# Hours\nOpen 9-5, Mon-Fri.', '/kb/returns.md': '# Returns\n30-day window, original method.',});
const agent = defineAgent({ id: 'support', model, instructions: 'Answer from the knowledge base. Use the workspace tool to find and read files.', workspace: fs, // auto-registers the read-only `workspace` tool});The workspace tool
Section titled “The workspace tool”AgentConfig.workspace accepts a FileSystem (read-only) or { fs, shell?, readOnly? }. The model
sees one tool named workspace with these ops:
| Op | Purpose |
|----|---------|
| ls | List a directory |
| read / cat | Read a file (offset/limit for windows) |
| grep | Regex search across files (g/i/m/s flags) |
| find | Glob match under a root |
| write | Write a file (throws EROFS when read-only) |
| edit | Find/replace within a file |
Output is capped — reads truncate at 2000 lines / 50KB (use offset/limit), grep at 200 hits,
each line at 500 chars — and truncation is flagged with truncated: true. edit throws on an
ambiguous match; pass replaceAll: true for multi-match replaces. Tools return data only.
Skills on the filesystem
Section titled “Skills on the filesystem”Skills can live on the workspace as SKILL.md folders (the agentskills.io
spec — the same format Claude, Mastra, and flue use). fsSkillStore discovers them and plugs into
AgentConfig.skills with zero extra wiring — progressive disclosure (metadata in the prompt, body
loaded on demand) is handled for you. See the Skills guide for the disclosure model.
import { defineAgent } from '@kuralle-agents/core';import { InMemoryFs, fsSkillStore } from '@kuralle-agents/fs';
const fs = new InMemoryFs({ '/skills/refunds/SKILL.md': '---\nname: refunds\ndescription: Handle refund requests.\n---\n\n# Refunds\n...', '/skills/refunds/references/policy.md': '# Policy\n30-day window applies.',});
const agent = defineAgent({ id: 'support', model, workspace: fs, skills: fsSkillStore(fs), // gives the agent load_skill + read_skill_resource});Shell (run commands)
Section titled “Shell (run commands)”Attach a Shell alongside the FileSystem and the runtime exposes a durable bash tool. Three
backends cover the platforms:
import { defineAgent } from '@kuralle-agents/core';import { virtualShell } from '@kuralle-agents/fs/shell'; // just-bash over an in-memory fs (Node / CF container)import { nodeShell } from '@kuralle-agents/fs/node'; // real host shell (env-allowlisted, process-tree kill)import { cloudflareShell } from '@kuralle-agents/fs/cloudflare'; // wraps a @cloudflare/sandbox Durable Object
const { fs, shell } = virtualShell({ initialFiles: { '/data/orders.txt': 'ORD-1\nORD-2\nORD-3\n' } });
const agent = defineAgent({ id: 'analyst', model, workspace: { fs, shell, readOnly: false }, tools: { /* bash is registered on the executor; expose it here or per node when you want the model to run commands */ },});Open Knowledge Format (OKF)
Section titled “Open Knowledge Format (OKF)”An OKF v0.1 bundle is just
markdown + YAML frontmatter + files — knowledge concepts (tables, metrics, runbooks) that cross-link
into a graph. Because it’s a directory of files, a kuralle workspace is an OKF consumption agent
with no adapter: the model navigates index.md → concept → bundle-relative links via read/grep.
import { okfBundleToFs, listOkfConcepts } from '@kuralle-agents/fs';
const fs = okfBundleToFs({ '/index.md': '# Sales\n* [Orders](/tables/orders.md) - one row per order.', '/tables/orders.md': '---\ntype: BigQuery Table\ntitle: Orders\n---\n# Schema\n...',});
const concepts = await listOkfConcepts(fs); // [{ id, type, title, description, links }]const agent = defineAgent({ id: 'analyst', model, workspace: fs });parseOkfConcept, listOkfConcepts, and okfBundleToFs follow the spec’s permissive consumption
model — only type is required; unknown fields and broken links are tolerated.
Persistence — pick a backend by platform
Section titled “Persistence — pick a backend by platform”InMemoryFs is ephemeral. For a workspace that survives restarts — so files, skills, and the durable
tool journal agree across process boundaries — use SqlFileSystem, a drop-in FileSystem over any
SQL handle (+ optional blob store for large files). Pass the handle your platform gives you:
// Node — built-in node:sqlite (Node >= 22.5)import { nodeSqlFileSystem } from '@kuralle-agents/fs/node';const fs = nodeSqlFileSystem('/data/agent.db');
// Serverless / edge (Vercel) — hosted SQLite (Turso / libSQL) over a zero-dependency// fetch-only backend: no @libsql/client, no native binary, no bundler banner.import { sqlFileSystem, libsqlHttpBackend } from '@kuralle-agents/fs';const fs = sqlFileSystem(libsqlHttpBackend({ url: env.TURSO_DATABASE_URL, authToken: env.TURSO_AUTH_TOKEN }));
// Bun / anything — wrap the SQLite handle in a 3-line SqlBackendimport { sqlFileSystem, type SqlBackend } from '@kuralle-agents/fs';import { Database } from 'bun:sqlite';const db = new Database('/data/agent.db');const backend: SqlBackend = { query: (s, ...p) => db.query(s).all(...p) as never, run: (s, ...p) => { db.query(s).run(...p); },};const fs = sqlFileSystem(backend);Everything above still works — workspace, bash, fsSkillStore, OKF — now durably:
const agent = defineAgent({ id: 'support', model, workspace: fs, skills: fsSkillStore(fs) });| Platform | Factory | Storage |
|----------|---------|---------|
| Cloudflare (DO) | sqlFileSystem(ctx.storage.sql) | Durable Object SQLite (+ R2 for large files) |
| Cloudflare (D1) | sqlFileSystem(env.DB) | D1 |
| Node | nodeSqlFileSystem(path) | node:sqlite file |
| Serverless / edge (Vercel) | sqlFileSystem(libsqlHttpBackend({url,authToken})) | Turso / libSQL over fetch (zero-dep) |
| Bun / custom | sqlFileSystem(backend) | any SqlBackend |
Nothing is enforced — a workspace is opt-in, and the backend is entirely yours: in-memory, on-disk
SQLite, DO SQLite, D1, hosted libSQL, or any SqlBackend. @kuralle-agents/fs has no @libsql/client
dependency; the fetch-only libsqlHttpBackend needs only fetch.
SqlFileSystem is a verified drop-in for InMemoryFs (identical behavior and error codes) and runs
on real Cloudflare workerd over a Durable Object’s ctx.storage.sql. Files above inlineThreshold
(default 1.5MB) spill to the BlobStore (R2); everything else stores inline.
On Cloudflare: a persistent workspace inside a cf-agent
Section titled “On Cloudflare: a persistent workspace inside a cf-agent”@kuralle-agents/cf-agent’s KuralleAgent extends Cloudflare’s AIChatAgent — it is a CF Agents
Durable Object, not a custom one. Give the agent a SqlFileSystem over its own ctx.storage.sql
in getAgents(), and its files persist in the same DO across turns and restarts:
import { KuralleAgent } from '@kuralle-agents/cf-agent';import { sqlFileSystem, createFsTool } from '@kuralle-agents/fs';import { defineAgent } from '@kuralle-agents/core';
export class WorkspaceAgent extends KuralleAgent<Env> { protected getAgents() { const fs = sqlFileSystem(this.ctx.storage.sql); // persistent, on this agent's DO SQLite return [ defineAgent({ id: 'workspace-agent', model, instructions: 'You have a persistent workspace. Use the workspace tool to read/write files.', workspace: { fs, readOnly: false }, tools: { workspace: createFsTool({ fs, readOnly: false }) }, }), ]; } protected getDefaultAgentId() { return 'workspace-agent'; }}See the live demo in apps/playground/fs-demo-cf (deployed to Cloudflare Workers). For large files,
pass { blobs: r2BlobStore(env.BUCKET) }; for D1 instead of DO SQLite, sqlFileSystem(env.DB).
Composite workspaces
Section titled “Composite workspaces”Federate several backends behind one filesystem with CompositeFileSystem — longest path-prefix
wins, and cp/mv work across mounts. Mark a mount read-only with a readOnly: true property.
import { CompositeFileSystem, InMemoryFs } from '@kuralle-agents/fs';
const fs = new CompositeFileSystem({ mounts: { '/docs': Object.assign(new InMemoryFs({ '/handbook.md': '# Handbook' }), { readOnly: true }), '/scratch': new InMemoryFs(), },});See also
Section titled “See also”- Skills — the progressive-disclosure model that
fsSkillStorefeeds. - Durable Execution — fs and shell tools execute fresh (
replay: false) rather than replaying a cached result; a persistentSqlFileSystemkeeps the fs and the journal in sync. - Tools — how the
workspaceandbashtools fit the durable tool model.