File I/O in Scala Golem Agents
Overview
Golem Scala agents are compiled to JavaScript via Scala.js and run in a QuickJS-based WASM runtime. The runtime provides node:fs for filesystem operations, accessible via Scala.js JavaScript interop. Standard JVM file I/O (java.io.File, java.nio.file.*) is not available.
To provision files into an agent’s filesystem, See the golem-add-initial-files guide. To understand the full runtime environment, See the golem-js-runtime guide.
Setting Up the node:fs Facade
Define a Scala.js facade object for the node:fs module:
import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport
@js.native
@JSImport("node:fs", JSImport.Namespace)
private object Fs extends js.Object {
def readFileSync(path: String, encoding: String): String = js.native
def readFileSync(path: String): js.typedarray.Uint8Array = js.native
def writeFileSync(path: String, data: String): Unit = js.native
def existsSync(path: String): Boolean = js.native
def readdirSync(path: String): js.Array[String] = js.native
def appendFileSync(path: String, data: String): Unit = js.native
def mkdirSync(path: String, options: js.Object): Unit = js.native
}Important: WASI modules like
node:fsare not available during the build-time pre-initialization (wizer) phase — they are only available at runtime. Use lazy val to defer initialization:// ✅ CORRECT — lazy val defers import to first runtime use private lazy val fs: Fs.type = Fs // ❌ WRONG — top-level val triggers import during pre-initialization and fails private val fs: Fs.type = Fs
Reading Files
Text Files
val content: String = Fs.readFileSync("/data/config.json", "utf-8")Binary Files
val bytes: js.typedarray.Uint8Array = Fs.readFileSync("/data/image.png")Writing Files
Only files provisioned with read-write permission (or files in non-provisioned paths) can be written to.
Fs.writeFileSync("/tmp/output.txt", "Hello, world!")Checking File Existence
if (Fs.existsSync("/data/config.json")) {
val content = Fs.readFileSync("/data/config.json", "utf-8")
}Listing Directories
val files: js.Array[String] = Fs.readdirSync("/data")
files.foreach(println)Complete Agent Example
import golem.runtime.annotations.{agentDefinition, agentImplementation}
import golem.BaseAgent
import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport
import scala.concurrent.Future
@js.native
@JSImport("node:fs", JSImport.Namespace)
private object Fs extends js.Object {
def readFileSync(path: String, encoding: String): String = js.native
def appendFileSync(path: String, data: String): Unit = js.native
}
@agentDefinition()
trait FileReaderAgent extends BaseAgent {
class Id(val name: String)
def readGreeting(): Future[String]
def writeLog(message: String): Future[Unit]
}
@agentImplementation()
final class FileReaderAgentImpl(private val name: String) extends FileReaderAgent {
override def readGreeting(): Future[String] = Future.successful {
Fs.readFileSync("/data/greeting.txt", "utf-8").trim
}
override def writeLog(message: String): Future[Unit] = Future.successful {
Fs.appendFileSync("/tmp/agent.log", message + "\n")
}
}Key Constraints
- Use
node:fsvia@JSImport— standard JVM file I/O (java.io.File,java.nio.file.*) does not work in Scala.js - Use lazy val or defer
node:fsaccess to method bodies to avoid pre-initialization failures - Files provisioned via
golem-add-initial-fileswithread-onlypermission 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)