Sagents.FileSystem.FileSystemState (Sagents v0.8.0-rc.7)

Copy Markdown

State management for the FileSystem.

This module handles all state transitions for the virtual filesystem. File entries are stored in an in-memory map within the GenServer state.

Summary

Functions

Deletes a file from the filesystem.

Check if a file exists in the filesystem.

Flushes all pending debounce timers by persisting files synchronously.

Lists all file entries in the filesystem (without loading content).

Lists all file paths in the filesystem.

Loads a file's content from persistence into ETS.

Moves a file or directory (and its children) from one path to another.

Creates a new FileSystemState.

Persists a file to storage (called when debounce timer fires).

Reads a file entry from the filesystem state.

Registers file entries in the filesystem.

Registers a new persistence configuration.

Reset the filesystem to pristine persisted state.

Computes filesystem statistics.

Writes a file to the filesystem.

Types

t()

@type t() :: %Sagents.FileSystem.FileSystemState{
  debounce_timers: %{required(String.t()) => reference()},
  files: %{required(String.t()) => Sagents.FileSystem.FileEntry.t()},
  persistence_configs: %{
    required(String.t()) => Sagents.FileSystem.FileSystemConfig.t()
  },
  publisher: Sagents.Publisher.State.t(),
  scope_key: term()
}

Functions

delete_file(state, path)

@spec delete_file(t(), String.t()) :: {:ok, t()} | {:error, term(), t()}

Deletes a file from the filesystem.

Returns {:ok, new_state} or {:error, reason, state}.

file_exists?(state, path)

@spec file_exists?(t(), String.t()) :: boolean()

Check if a file exists in the filesystem.

Examples

iex> FileSystemState.file_exists?(state, "/notes.txt")
true

iex> FileSystemState.file_exists?(state, "/nonexistent.txt")
false

flush_all(state)

@spec flush_all(t()) :: t()

Flushes all pending debounce timers by persisting files synchronously.

Returns updated state with cleared timers.

list_entries(state)

@spec list_entries(t()) :: [Sagents.FileSystem.FileEntry.t()]

Lists all file entries in the filesystem (without loading content).

Returns entries with metadata but content NOT loaded — suitable for building sidebar trees or LLM list_files tool responses.

list_files(state)

@spec list_files(t()) :: [String.t()]

Lists all file paths in the filesystem.

Returns paths for both memory and persisted files, regardless of load status.

Parameters

  • state - Current FileSystemState

Examples

iex> FileSystemState.list_files(state)
["/file1.txt", "/Memories/file2.txt"]

load_file(state, path)

@spec load_file(t(), String.t()) :: {:ok, t()} | {:error, term(), t()}

Loads a file's content from persistence into ETS.

Called by FileSystemServer when a file needs to be lazy-loaded. If the file is already loaded or is memory-only, returns {:ok, state} without changes.

Returns

  • {:ok, state} - File loaded successfully (or already loaded)
  • {:error, reason, state} - Failed to load from persistence

move_file(state, path, path)

@spec move_file(t(), String.t(), String.t()) ::
  {:ok, [Sagents.FileSystem.FileEntry.t()], t()} | {:error, term(), t()}

Moves a file or directory (and its children) from one path to another.

This is an atomic re-key operation that does not trigger delete_from_storage or create new entries. Instead, it:

  1. Re-keys entries in the files map from old path to new path
  2. Updates the path field on each entry
  3. Transfers any pending debounce timers to the new paths
  4. Calls the optional move_in_storage/3 persistence callback so backends can update their path references
  5. If the backend doesn't implement move_in_storage/3, marks entries as dirty_non_content so the next persist cycle pushes the changes

Returns {:ok, moved_entries, new_state} or {:error, reason, state}.

new(opts)

@spec new(keyword()) :: {:ok, t()} | {:error, term()}

Creates a new FileSystemState.

Options

  • :scope_key - Scope identifier (required) - Can be any term that uniquely identifies the scope
    • Tuple: {:user, 123}, {:agent, uuid}, {:project, id}
    • UUID: "550e8400-e29b-41d4-a716-446655440000"
    • Database ID: 12345 or "12345"
  • :configs - List of FileSystemConfig structs (optional, default: [])

persist_file(state, path)

@spec persist_file(t(), String.t()) :: t()

Persists a file to storage (called when debounce timer fires).

Returns updated state.

read_file(state, path)

@spec read_file(t(), String.t()) ::
  {:ok, Sagents.FileSystem.FileEntry.t()} | {:error, :enoent}

Reads a file entry from the filesystem state.

Parameters

  • state - Current FileSystemState
  • path - The file path to read

Returns

  • {:ok, entry} - File entry found
  • {:error, :enoent} - File not found

register_files(state, file_entries)

@spec register_files(t(), [Sagents.FileSystem.FileEntry.t()]) :: {:ok, t()}

Registers file entries in the filesystem.

This is useful for pre-populating the filesystem with file metadata without loading content. For example, used by tests or for in-memory only files.

Parameters

  • state - Current FileSystemState
  • file_entries - List of FileEntry structs to register

Returns

  • {:ok, new_state} on success

Examples

iex> {:ok, entry} = FileEntry.new_memory_file("/scratch/temp.txt", "data")
iex> {:ok, new_state} = FileSystemState.register_files(state, [entry])

register_persistence(state, config)

@spec register_persistence(t(), Sagents.FileSystem.FileSystemConfig.t()) ::
  {:ok, t()} | {:error, term()}

Registers a new persistence configuration.

When a persistence config is registered, this function calls the persistence module's list_persisted_files/2 callback to discover existing files and adds them to the filesystem with loaded: false (lazy loading).

Parameters

  • state - Current FileSystemState
  • config - FileSystemConfig to register

Returns

  • {:ok, new_state} on success
  • {:error, reason} if base_directory already registered

Examples

iex> config = FileSystemConfig.new!(%{
...>   base_directory: "user_files",
...>   persistence_module: MyApp.Persistence.Disk
...> })
iex> {:ok, new_state} = FileSystemState.register_persistence(state, config)

reset(state)

@spec reset(t()) :: t()

Reset the filesystem to pristine persisted state.

This clears:

  • All memory-only files (completely removed)
  • All in-memory modifications to persisted files (discarded)
  • All dirty flags (no persistence of pending changes)
  • All debounce timers (pending writes cancelled)

Then re-indexes all persisted files from storage backends, ensuring:

  • Fresh metadata from storage (picks up any external changes)
  • Files marked as unloaded (will lazy-load on next access)
  • Latest list of persisted files (includes files created during execution)

Uses the existing index_persisted_files/1 code path for consistency.

Returns

Updated state with reset filesystem.

Examples

iex> state = FileSystemState.reset(state)

stats(state)

@spec stats(t()) :: map()

Computes filesystem statistics.

write_file(state, path, content, opts \\ [])

@spec write_file(t(), String.t(), String.t(), keyword()) ::
  {:ok, Sagents.FileSystem.FileEntry.t(), t()} | {:error, term(), t()}

Writes a file to the filesystem.

For existing files, preserves existing metadata (custom, created_at, mime_type) and only updates content-related fields. For new files, creates a fresh entry.

Returns {:ok, entry, new_state} or {:error, reason, state}.