Skip to Content
How-To GuidesMoonBitFile I/O in MoonBit Golem Agents

File I/O in MoonBit Golem Agents

Overview

MoonBit Golem agents can access files provisioned into the agent’s filesystem via golem.yaml. File access uses the SDK’s @fs package (golemcloud/golem_sdk/filesystem), which provides convenience functions and re-exports of the necessary types.

To provision files into an agent’s filesystem, See the golem-add-initial-files guide.

Prerequisites: Adding the Dependency

Add the filesystem package to your agent’s moon.pkg:

import { "golemcloud/golem_sdk/filesystem" @fs, }

No WIT changes or binding regeneration is needed — the SDK already includes the filesystem imports.

File Provisioning

Files must first be provisioned via golem.yaml (see golem-add-initial-files guide). Provisioned files are available through preopened directories. The preopened directory for provisioned files is typically mounted at /.

Reading Files

Getting the Root Directory

Use @fs.get_root_dir() to get the preopened directory at /:

///| fn get_root() -> @fs.Descriptor { @fs.get_root_dir().unwrap() }

Or use @fs.get_preopened_dir(path) for a specific mount point.

Reading a Text File

Use the convenience function @fs.read_string:

///| fn read_file(path : String) -> String!Error { let root = @fs.get_root_dir().unwrap() @fs.read_string(root, path).unwrap() }

Reading a Binary File

Use @fs.read_bytes:

///| fn read_binary(path : String) -> FixedArray[Byte]!Error { let root = @fs.get_root_dir().unwrap() @fs.read_bytes(root, path).unwrap() }

Writing Files

Only files provisioned with read-write permission (or files in non-provisioned paths) can be written to.

Writing a Text File

///| fn write_file(path : String, content : String) -> Unit!Error { let root = @fs.get_root_dir().unwrap() @fs.write_string(root, path, content).unwrap() }

Writing Binary Data

///| fn write_binary(path : String, data : FixedArray[Byte]) -> Unit!Error { let root = @fs.get_root_dir().unwrap() @fs.write_bytes(root, path, data).unwrap() }

Listing Directory Entries

///| fn list_dir(path : String) -> Array[String]!Error { let root = @fs.get_root_dir().unwrap() @fs.list_directory(root, path).unwrap() }

Low-Level Access

For more control (e.g., opening with specific flags, using streams, stat), use the re-exported types directly from @fs:

///| fn open_read_only(path : String) -> @fs.Descriptor!Error { let root = @fs.get_root_dir().unwrap() root .open_at( @fs.PathFlags::default(), path, @fs.OpenFlags::default(), @fs.DescriptorFlags::default().set(@fs.READ), ) .unwrap() }

Available re-exported types from @fs: Descriptor, DirectoryEntryStream, DescriptorFlags, PathFlags, OpenFlags, ErrorCode, DirectoryEntry, DescriptorStat, NewTimestamp, DescriptorType, Advice, MetadataHashValue, DescriptorFlagsFlag (READ, WRITE, etc.), PathFlagsFlag, OpenFlagsFlag (CREATE, DIRECTORY, EXCLUSIVE, TRUNCATE).

Complete Agent Example

/// File reader agent that reads provisioned files #derive.agent struct FileReader { name : String } fn FileReader::new(name : String) -> FileReader { { name } } /// Reads the content of a provisioned text file pub fn FileReader::read_text(self : Self, path : String) -> String { let _ = self let root = @fs.get_root_dir().unwrap() @fs.read_string(root, path).unwrap() } /// Writes content to a file (must be writable) pub fn FileReader::write_text(self : Self, path : String, content : String) -> Unit { let _ = self let root = @fs.get_root_dir().unwrap() @fs.write_string(root, path, content).unwrap() } /// Lists entries in a directory pub fn FileReader::list_dir(self : Self, path : String) -> Array[String] { let _ = self let root = @fs.get_root_dir().unwrap() @fs.list_directory(root, path).unwrap() }

Key Constraints

  • Files provisioned via golem-add-initial-files with read-only permission cannot be written to
  • The filesystem is per-agent-instance — each agent has its own isolated filesystem
  • File changes within an agent are persistent across invocations (durable state)
  • All paths are relative to a preopened directory descriptor — there is no global filesystem root; you must obtain a descriptor via get_root_dir() or get_preopened_dir(path)

Host Read-Size Limit (Important)

The underlying WASI host (wasmtime) caps each Descriptor::read call at MAX_READ_SIZE_ALLOC = 64 * 1024 bytes (64 KiB). Any single read request larger than that is silently truncated by the host, so a naive read(stat.size, 0) will only return the first 64 KiB of any larger file.

  • When using the SDK wrappers (@fs.read_bytes, @fs.read_string): the wrappers already handle this — they perform chunked reads at increasing offsets and concatenate the results, so they correctly return the full contents regardless of file size.

  • When calling Descriptor::read directly (low-level access via the re-exported types from @fs, or directly through the WASI filesystem/types interface): you must loop, requesting at most 64 KiB per call, advancing the offset by the number of bytes actually returned, and stopping when the EOF flag is set or the returned chunk is empty. Example skeleton:

    let chunks : Array[FixedArray[Byte]] = [] let mut offset : UInt64 = 0 let mut done = false while not(done) { match fd.read(65536, offset) { Err(e) => return Err(e) Ok((chunk, eof)) => { if chunk.length() > 0 { chunks.push(chunk) offset = offset + chunk.length().to_uint64() } if eof || chunk.length() == 0 { done = true } } } }

    Prefer the SDK wrappers unless you specifically need streaming or partial-read semantics.

Last updated on