Skip to content

Add a file-backed HistoryProvider #59

Description

@shibayan

Motivation

The only bundled HistoryProvider is InMemoryHistoryProvider, so a session does not survive a
process restart without a custom implementation. Of the reference implementations only Python
ships a file-backed one (FileHistoryProvider, JSON Lines); .NET has in-memory, Cosmos DB and
Valkey providers, and Go has in-memory only. Adopting Python's on-disk format means an external
provider (blob storage, a database) can be written against a format this framework also reads.

Proposed API

Node's fs is required, so this lands on a Node-only subpath rather than the runtime-agnostic
root entry, which has to keep working on Deno, Bun, edge runtimes and in browsers:

import { FileHistoryProvider } from '@polymind-inc/agent-framework/node';

const provider = new FileHistoryProvider({ storagePath: '/var/lib/sessions' });

Behaviour

  • One append-only file per session: {storagePath}/{sessionId}.jsonl. The directory is created
    when missing. The default sourceId is file_history, matching Python.
  • One message per line: JSON.stringify(serializeMessage(msg)) followed by \n, appended, UTF-8.
  • getMessages returns [] when the file does not exist. Blank lines are skipped rather than
    rejected; a line that fails to parse raises an error naming the line number.
  • A session id that is not safe as a filename is encoded rather than rejected (Python uses a
    ~session- prefix plus an encoded form), and the resolved path is verified to stay inside
    storagePath.
  • Appends to one file are serialized through a per-path async lock, so concurrent runs on the same
    session cannot interleave partial lines.

Acceptance criteria

  • Round trip: save through one instance, read the transcript back through another.
  • Order is preserved across several turns of appending.
  • A session id containing ../, an absolute path or a path separator cannot write outside
    storagePath.
  • The JSONL layout is pinned by a test — external providers implement against it.

Metadata

Metadata

Assignees

No one assigned

    Labels

    coreUsage: [Issues, PRs], Target: packages/core

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions