Skip to Content
Next — You're reading the unreleased preview docs. APIs and pages may change before the next release.Switch to the current docs →
How-To GuidesMoonBitFile I/O in MoonBit Golem Agents

File I/O in MoonBit Golem Agents

Use golemcloud/golem_sdk/filesystem for Preview 3 filesystem access. Provision initial files through golem.yaml; load golem-add-initial-files for that setup. No WIT changes or binding regeneration are required in the consumer project.

Setup

Add to moon.pkg:

import { "golemcloud/golem_sdk/filesystem" @fs, "golemcloud/golem_sdk/interface/wasi/filesystem/types" @fsTypes, }

@fs.get_root_dir() returns the preopened directory mounted at /, or None. Use @fs.get_preopened_dir(path) for another mount. Paths are relative to that descriptor; SDK convenience functions also strip leading / characters.

Reading, Writing and Listing

These helpers are async, as are methods and helpers that call them. MoonBit has no await keyword. The examples return the filesystem Result so the caller can handle errors instead of aborting on .unwrap():

async fn read_file(path : String) -> Result[String, @fsTypes.ErrorCode] { let root = @fs.get_root_dir().unwrap() defer root.drop() @fs.read_string(root, path) } async fn read_binary(path : String) -> Result[FixedArray[Byte], @fsTypes.ErrorCode] { let root = @fs.get_root_dir().unwrap() defer root.drop() @fs.read_bytes(root, path) } async fn write_file(path : String, content : String) -> Result[Unit, @fsTypes.ErrorCode] { let root = @fs.get_root_dir().unwrap() defer root.drop() @fs.write_string(root, path, content) } async fn write_binary(path : String, data : FixedArray[Byte]) -> Result[Unit, @fsTypes.ErrorCode] { let root = @fs.get_root_dir().unwrap() defer root.drop() @fs.write_bytes(root, path, data) } async fn list_dir(path : String) -> Result[Array[String], @fsTypes.ErrorCode] { let root = @fs.get_root_dir().unwrap() defer root.drop() @fs.list_directory(root, path) }

The examples require a / preopen. Handle None explicitly if your deployment does not guarantee one. read_string decodes UTF-8 with replacement for invalid sequences. Use read_bytes and a strict UTF-8 decoder to detect malformed text. Writes create or truncate a file. Provisioned read-only files cannot be written.

Complete Agent Example

#derive.agent struct FileReader { name : String } fn FileReader::new(name : String) -> FileReader { { name } } pub async fn FileReader::read_text(self : Self, path : String) -> Result[String, String] { let root = @fs.get_root_dir().unwrap() defer root.drop() match @fs.read_string(root, path) { Ok(text) => Ok(text) Err(_) => Err("File read failed") } } pub async fn FileReader::write_text(self : Self, path : String, content : String) -> Result[Unit, String] { let root = @fs.get_root_dir().unwrap() defer root.drop() match @fs.write_string(root, path, content) { Ok(_) => Ok(()) Err(_) => Err("File write failed") } } pub async fn FileReader::list_dir(self : Self, path : String) -> Result[Array[String], String] { let root = @fs.get_root_dir().unwrap() defer root.drop() match @fs.list_directory(root, path) { Ok(entries) => Ok(entries) Err(_) => Err("Directory listing failed") } } fn main {}

The agent maps the raw WASI error enum to a schema-supported application error (String) instead of exposing @fsTypes.ErrorCode in its public agent interface.

Low-Level Streams and Resource Ownership

Prefer the convenience wrappers: they consume the full stream, inspect its completion result and drop descriptors they open. There is no Preview 2 Descriptor::read(size, offset) API or (bytes, eof) tuple in the current contract. A stream can return multiple chunks regardless of file size.

For partial reads, use Descriptor::read_via_stream(offset), which returns a byte stream and a completion future. This fragment belongs inside an async function, with fd an already-open descriptor:

let (contents, completed) = fd.read_via_stream(0) defer contents.drop() defer completed.drop() let chunks : Array[FixedArray[Byte]] = [] for ;; { match contents.read(65536) { Some(chunk) => chunks.push(chunk) None => break } } // Stream end alone does not establish success. Inspect the completion result. let result : Result[Unit, @fsTypes.ErrorCode] = completed.get()

Handle result and combine the chunks only on success. Release fd when its owner is finished; use defer fd.drop() after opening it. Opening with open_at requires a relative path plus PathFlags, OpenFlags and DescriptorFlags. Writing uses write_via_stream with an async producer and a completion future, not blocking writes. Directory listing uses an entry stream and completion future, not the removed DirectoryEntryStream.

Persistence

  • Filesystems are isolated per agent instance.
  • File changes persist across invocations of a durable agent.
  • Read-only provisioned files remain read-only; writable files need the correct provisioning permissions.
  • Use SDK wrappers unless partial-read or streaming behavior is required. Their bounded chunk reads collect the entire file, not just the first 64 KiB.