@simulacra-ai/session 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -1
- package/dist/index.cjs +79 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +154 -1
- package/dist/index.d.ts +154 -1
- package/dist/index.js +78 -0
- package/dist/index.js.map +1 -1
- package/package.json +17 -5
package/README.md
CHANGED
|
@@ -84,7 +84,7 @@ Checkpoint children have `auto_slug` disabled since their sessions are internal.
|
|
|
84
84
|
|
|
85
85
|
## Session Storage
|
|
86
86
|
|
|
87
|
-
A `SessionStore` is the storage backend that a `SessionManager` reads from and writes to. The store handles listing, loading, saving, and deleting sessions.
|
|
87
|
+
A `SessionStore` is the storage backend that a `SessionManager` reads from and writes to. The store handles listing, loading, saving, and deleting sessions. Three stores are included out of the box.
|
|
88
88
|
|
|
89
89
|
**FileSessionStore** persists sessions as JSON files on disk. Each session is a single `{id}.json` file. Child session relationships are indexed using hard links under `{parent-id}-forks/` directories.
|
|
90
90
|
|
|
@@ -98,6 +98,35 @@ const store = new FileSessionStore("./data/sessions");
|
|
|
98
98
|
const store = new InMemorySessionStore();
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
+
**DrizzleSessionStore** persists sessions in a relational database using Drizzle ORM. It works with any database that Drizzle supports (PostgreSQL, MySQL, SQLite). The store does not import `drizzle-orm` itself. Instead, it accepts an adapter object with `list`, `load`, `upsert`, and `delete` functions that wrap Drizzle queries against the application's table.
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
import { DrizzleSessionStore } from "@simulacra-ai/session";
|
|
105
|
+
|
|
106
|
+
const store = new DrizzleSessionStore({
|
|
107
|
+
list: () =>
|
|
108
|
+
db.select().from(sessionsTable).orderBy(desc(sessionsTable.updated_at)),
|
|
109
|
+
load: async (id) => {
|
|
110
|
+
const [row] = await db
|
|
111
|
+
.select({ metadata: sessionsTable.metadata, messages: sessionsTable.messages })
|
|
112
|
+
.from(sessionsTable)
|
|
113
|
+
.where(eq(sessionsTable.id, id));
|
|
114
|
+
return row;
|
|
115
|
+
},
|
|
116
|
+
upsert: (row) =>
|
|
117
|
+
db.insert(sessionsTable).values(row).onConflictDoUpdate({
|
|
118
|
+
target: sessionsTable.id,
|
|
119
|
+
set: { metadata: row.metadata, messages: row.messages, updated_at: row.updated_at },
|
|
120
|
+
}),
|
|
121
|
+
delete: async (id) => {
|
|
122
|
+
const result = await db.delete(sessionsTable).where(eq(sessionsTable.id, id));
|
|
123
|
+
return result.rowCount > 0;
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The `DrizzleSessionRow` and `DrizzleSessionAdapter` types are exported for reference when defining a table schema and adapter. See the JSDoc on `DrizzleSessionRow` for example PostgreSQL and SQLite table definitions.
|
|
129
|
+
|
|
101
130
|
Custom storage backends (databases, cloud storage, key-value stores) can be built by implementing the `SessionStore` interface. The [extensibility guide](EXTENSIBILITY.md) covers the interface, implementation notes, and includes a full example.
|
|
102
131
|
|
|
103
132
|
## License
|
package/dist/index.cjs
CHANGED
|
@@ -30,6 +30,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
+
DrizzleSessionStore: () => DrizzleSessionStore,
|
|
33
34
|
FileSessionStore: () => FileSessionStore,
|
|
34
35
|
InMemorySessionStore: () => InMemorySessionStore,
|
|
35
36
|
SessionManager: () => SessionManager
|
|
@@ -605,8 +606,86 @@ var InMemorySessionStore = class {
|
|
|
605
606
|
return this.#sessions.delete(id);
|
|
606
607
|
}
|
|
607
608
|
};
|
|
609
|
+
|
|
610
|
+
// src/drizzle-session-store.ts
|
|
611
|
+
var DrizzleSessionStore = class {
|
|
612
|
+
#adapter;
|
|
613
|
+
constructor(adapter) {
|
|
614
|
+
this.#adapter = adapter;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Lists all sessions, sorted by most recently updated first.
|
|
618
|
+
*
|
|
619
|
+
* @returns A promise that resolves to an array of session metadata.
|
|
620
|
+
*/
|
|
621
|
+
async list() {
|
|
622
|
+
const rows = await this.#adapter.list();
|
|
623
|
+
return rows.map((row) => ({
|
|
624
|
+
id: row.id,
|
|
625
|
+
...row.metadata
|
|
626
|
+
}));
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Loads a session by its ID.
|
|
630
|
+
*
|
|
631
|
+
* @param id - The unique identifier of the session to load.
|
|
632
|
+
* @returns A promise that resolves to the session metadata and messages, or undefined if not found.
|
|
633
|
+
*/
|
|
634
|
+
async load(id) {
|
|
635
|
+
const row = await this.#adapter.load(id);
|
|
636
|
+
if (!row) {
|
|
637
|
+
return void 0;
|
|
638
|
+
}
|
|
639
|
+
return {
|
|
640
|
+
metadata: {
|
|
641
|
+
id,
|
|
642
|
+
...row.metadata
|
|
643
|
+
},
|
|
644
|
+
messages: row.messages
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
/**
|
|
648
|
+
* Saves a session with the given messages and metadata.
|
|
649
|
+
*
|
|
650
|
+
* Creates a new session if the ID does not exist, or updates the existing session.
|
|
651
|
+
* Automatically updates the `updated_at` timestamp and `message_count`.
|
|
652
|
+
*
|
|
653
|
+
* @param id - The unique identifier of the session.
|
|
654
|
+
* @param messages - The messages to store for this session.
|
|
655
|
+
* @param metadata - Optional partial metadata to merge with existing metadata.
|
|
656
|
+
* @returns A promise that resolves when the save operation is complete.
|
|
657
|
+
*/
|
|
658
|
+
async save(id, messages, metadata) {
|
|
659
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
660
|
+
const existing = await this.#adapter.load(id);
|
|
661
|
+
const existing_metadata = existing?.metadata;
|
|
662
|
+
const merged = {
|
|
663
|
+
...existing_metadata,
|
|
664
|
+
created_at: existing_metadata?.created_at ?? now,
|
|
665
|
+
updated_at: now,
|
|
666
|
+
message_count: messages.length,
|
|
667
|
+
...metadata
|
|
668
|
+
};
|
|
669
|
+
await this.#adapter.upsert({
|
|
670
|
+
id,
|
|
671
|
+
metadata: merged,
|
|
672
|
+
messages,
|
|
673
|
+
updated_at: now
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
/**
|
|
677
|
+
* Deletes a session by its ID.
|
|
678
|
+
*
|
|
679
|
+
* @param id - The unique identifier of the session to delete.
|
|
680
|
+
* @returns A promise that resolves to true if the session was deleted, false if it was not found.
|
|
681
|
+
*/
|
|
682
|
+
async delete(id) {
|
|
683
|
+
return this.#adapter.delete(id);
|
|
684
|
+
}
|
|
685
|
+
};
|
|
608
686
|
// Annotate the CommonJS export names for ESM import in node:
|
|
609
687
|
0 && (module.exports = {
|
|
688
|
+
DrizzleSessionStore,
|
|
610
689
|
FileSessionStore,
|
|
611
690
|
InMemorySessionStore,
|
|
612
691
|
SessionManager
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/session-manager.ts","../src/file-session-store.ts","../src/in-memory-session-store.ts"],"sourcesContent":["export type {\n SessionMetadata,\n SessionStore,\n SessionManagerOptions,\n SessionManagerEvents,\n} from \"./types.ts\";\n\nexport { SessionManager } from \"./session-manager.ts\";\nexport { FileSessionStore } from \"./file-session-store.ts\";\nexport { InMemorySessionStore } from \"./in-memory-session-store.ts\";\n","import { randomUUID } from \"node:crypto\";\nimport EventEmitter from \"node:events\";\nimport type { Conversation, Message } from \"@simulacra-ai/core\";\nimport type {\n SessionMetadata,\n SessionStore,\n SessionManagerOptions,\n SessionManagerEvents,\n} from \"./types.ts\";\n\n/**\n * Manages conversation sessions including creation, loading, saving, and forking.\n *\n * The SessionManager acts as a bridge between a Conversation instance and a SessionStore,\n * handling session lifecycle, automatic saving, and session tree management through forking.\n * It supports disposable resource management and event emission for session operations.\n */\nexport class SessionManager {\n readonly #store: SessionStore;\n readonly #conversation: Conversation;\n readonly #auto_save: boolean;\n readonly #auto_slug: boolean;\n readonly #event_emitter = new EventEmitter<SessionManagerEvents>();\n readonly #child_sessions: SessionManager[] = [];\n\n #session_id?: string;\n #fork_offset = 0;\n #has_label = false;\n #disposed = false;\n\n /**\n * Creates a new SessionManager instance.\n *\n * @param store - The storage backend to use for persisting sessions.\n * @param conversation - The conversation instance to manage.\n * @param options - Optional configuration for session management behavior.\n */\n constructor(store: SessionStore, conversation: Conversation, options?: SessionManagerOptions) {\n this.#store = store;\n this.#conversation = conversation;\n this.#auto_save = options?.auto_save ?? true;\n this.#auto_slug = options?.auto_slug ?? true;\n\n if (this.#auto_save) {\n this.#conversation.on(\"message_complete\", this.#on_message_complete);\n }\n this.#conversation.on(\"create_child\", this.#on_create_child);\n this.#conversation.once(\"dispose\", this.#on_conversation_dispose);\n }\n\n /**\n * The ID of the currently active session.\n *\n * @returns The session ID if a session is active, otherwise undefined.\n */\n get current_session_id() {\n return this.#session_id;\n }\n\n /**\n * Whether a session is currently loaded.\n *\n * @returns True if a session is active, false otherwise.\n */\n get is_loaded() {\n return !!this.#session_id;\n }\n\n /**\n * Disposes of the SessionManager and cleans up resources.\n *\n * This method removes event listeners, disposes child sessions, and emits the dispose event.\n * It is called automatically when the associated conversation is disposed or when using\n * explicit resource management.\n */\n [Symbol.dispose]() {\n if (this.#disposed) {\n return;\n }\n this.#disposed = true;\n\n for (const child of this.#child_sessions) {\n child[Symbol.dispose]();\n }\n this.#child_sessions.length = 0;\n\n this.#conversation.off(\"message_complete\", this.#on_message_complete);\n this.#conversation.off(\"create_child\", this.#on_create_child);\n this.#conversation.off(\"dispose\", this.#on_conversation_dispose);\n this.#event_emitter.emit(\"dispose\");\n this.#event_emitter.removeAllListeners();\n }\n\n /**\n * Starts a new session with a freshly generated ID.\n *\n * This method clears the conversation history and creates a new session. If auto_save\n * is enabled or a label is provided, the session is immediately saved to the store.\n *\n * @param label - Optional label to assign to the new session.\n * @returns The generated session ID.\n */\n start_new(label?: string): string {\n this.#session_id = randomUUID();\n this.#fork_offset = 0;\n this.#has_label = !!label;\n if (this.#conversation.messages.length > 0) {\n this.#conversation.clear();\n }\n if (label || this.#auto_save) {\n this.save({ label }).catch((error) => {\n this.#event_emitter.emit(\"lifecycle_error\", {\n error,\n operation: \"save\",\n context: { session_id: this.#session_id },\n });\n });\n }\n return this.#session_id;\n }\n\n /**\n * Creates a new session as a fork of an existing parent session.\n *\n * A fork creates a new session that inherits messages from the parent session up to\n * the fork point. If detached, the fork does not inherit any messages from the parent\n * but still maintains a parent reference for organizational purposes.\n *\n * @param parent_session_id - The ID of the session to fork from.\n * @param options - Optional configuration for the fork operation.\n * @param options.detached - Whether to create a detached fork that does not inherit parent messages.\n * @returns A promise that resolves to the generated session ID for the fork.\n */\n async fork(parent_session_id: string, options?: { detached?: boolean }): Promise<string> {\n const detached = options?.detached ?? false;\n this.#session_id = randomUUID();\n this.#has_label = false;\n if (!detached) {\n this.#conversation.clear();\n }\n\n let fork_message_id: string | undefined;\n\n if (!detached) {\n const messages = await this.#resolve_messages(parent_session_id);\n if (messages.length > 0) {\n this.#conversation.load(messages);\n this.#fork_offset = messages.length;\n fork_message_id = messages.at(-1)?.id;\n } else {\n this.#fork_offset = 0;\n }\n } else {\n this.#fork_offset = 0;\n }\n\n const parent_result = await this.#store.load(parent_session_id);\n if (parent_result?.metadata.checkpoint_state) {\n this.#conversation.checkpoint_state = parent_result.metadata.checkpoint_state;\n }\n\n await this.save({\n parent_id: parent_session_id,\n fork_message_id,\n detached,\n });\n\n return this.#session_id;\n }\n\n /**\n * Loads a session by ID or loads the most recent session if no ID is provided.\n *\n * If no session ID is provided and no sessions exist in the store, this method\n * starts a new session automatically. Loading a session resolves its full message\n * history by recursively following parent references.\n *\n * @param id - The ID of the session to load, or undefined to load the most recent session.\n * @returns A promise that resolves when the session is loaded.\n * @throws {Error} If the specified session ID is not found in the store.\n */\n async load(id?: string): Promise<void> {\n if (id) {\n const messages = await this.#resolve_messages(id);\n const result = await this.#store.load(id);\n if (!result) {\n throw new Error(`session not found: ${id}`);\n }\n this.#session_id = id;\n this.#has_label = !!result.metadata.label;\n this.#conversation.clear();\n this.#fork_offset = messages.length - result.messages.length;\n this.#conversation.load(messages);\n this.#conversation.checkpoint_state = result.metadata.checkpoint_state;\n this.#emit_load(messages);\n return;\n }\n\n const sessions = await this.#store.list();\n if (!sessions.length) {\n this.start_new();\n return;\n }\n\n const latest = sessions[0];\n return this.load(latest.id);\n }\n\n /**\n * Saves the current session to the store.\n *\n * This method persists only the messages owned by this session, excluding any messages\n * inherited from parent sessions. If auto_slug is enabled and no label has been set,\n * a label is automatically generated from the first user message.\n *\n * @param metadata - Optional partial metadata to update on the session.\n * @returns A promise that resolves when the save operation is complete.\n * @throws {Error} If no session is currently active.\n */\n async save(\n metadata?: Partial<\n Pick<\n SessionMetadata,\n \"label\" | \"provider\" | \"model\" | \"parent_id\" | \"fork_message_id\" | \"detached\"\n >\n >,\n ) {\n if (!this.#session_id) {\n throw new Error(\"no active session\");\n }\n const all_messages = this.#conversation.messages;\n const owned_messages = [...all_messages].slice(this.#fork_offset);\n\n if (this.#auto_slug && !metadata?.label && !this.#has_label) {\n const slug = SessionManager.#derive_slug(all_messages);\n if (slug) {\n metadata = { ...metadata, label: slug };\n this.#has_label = true;\n }\n }\n\n const conversation_metadata: Partial<SessionMetadata> = { ...metadata };\n if (this.#conversation.is_checkpoint) {\n conversation_metadata.is_checkpoint = true;\n }\n const checkpoint_state = this.#conversation.checkpoint_state;\n if (checkpoint_state) {\n conversation_metadata.checkpoint_state = { ...checkpoint_state };\n }\n\n await this.#store.save(this.#session_id, owned_messages, conversation_metadata);\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this.#event_emitter as any).emit(\"save\", {\n id: this.#session_id,\n messages: Object.freeze([...all_messages]),\n });\n }\n\n /**\n * Lists all sessions stored in the store.\n *\n * @returns A promise that resolves to an array of session metadata, typically sorted by last update time.\n */\n async list() {\n return this.#store.list();\n }\n\n /**\n * Deletes a session from the store.\n *\n * If the deleted session is currently active, the conversation is cleared and the\n * current session is unset.\n *\n * @param id - The ID of the session to delete.\n * @returns A promise that resolves to true if the session was deleted, false if it was not found.\n */\n async delete(id: string) {\n if (id === this.#session_id) {\n this.#session_id = undefined;\n this.#conversation.clear();\n }\n return this.#store.delete(id);\n }\n\n /**\n * Renames a session by updating its label.\n *\n * @param id - The ID of the session to rename.\n * @param label - The new label to assign to the session.\n * @returns A promise that resolves when the rename operation is complete.\n * @throws {Error} If the specified session ID is not found in the store.\n */\n async rename(id: string, label: string) {\n const result = await this.#store.load(id);\n if (!result) {\n throw new Error(`session not found: ${id}`);\n }\n await this.#store.save(id, result.messages, { label });\n if (id === this.#session_id) {\n this.#has_label = true;\n }\n }\n\n /**\n * Registers an event listener for the specified event.\n *\n * @param event - The name of the event to listen for.\n * @param listener - The callback function to invoke when the event is emitted.\n * @returns This SessionManager instance for method chaining.\n */\n on<E extends keyof SessionManagerEvents>(\n event: E,\n listener: (...args: SessionManagerEvents[E]) => void,\n ): this {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this.#event_emitter as any).on(event, listener);\n return this;\n }\n\n /**\n * Removes an event listener for the specified event.\n *\n * @param event - The name of the event to stop listening for.\n * @param listener - The callback function to remove.\n * @returns This SessionManager instance for method chaining.\n */\n off<E extends keyof SessionManagerEvents>(\n event: E,\n listener: (...args: SessionManagerEvents[E]) => void,\n ): this {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this.#event_emitter as any).off(event, listener);\n return this;\n }\n\n #emit_load(messages: Readonly<Message[]>) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this.#event_emitter as any).emit(\"load\", {\n id: this.#session_id,\n messages: Object.freeze([...messages]),\n });\n }\n\n async #resolve_messages(id: string): Promise<Message[]> {\n const result = await this.#store.load(id);\n if (!result) {\n return [];\n }\n\n const { metadata, messages } = result;\n\n if (metadata.parent_id && !metadata.detached) {\n const parent_messages = await this.#resolve_messages(metadata.parent_id);\n if (metadata.fork_message_id) {\n const cut = parent_messages.findIndex((m) => m.id === metadata.fork_message_id);\n if (cut >= 0) {\n return [...parent_messages.slice(0, cut + 1), ...messages];\n }\n }\n return [...parent_messages, ...messages];\n }\n\n return messages;\n }\n\n static #derive_slug(messages: readonly Readonly<Message>[]): string | undefined {\n const first_user = messages.find((m) => m.role === \"user\");\n if (!first_user) {\n return undefined;\n }\n\n const text_content = first_user.content.find((c) => c.type === \"text\");\n if (!text_content || text_content.type !== \"text\" || !text_content.text) {\n return undefined;\n }\n\n const raw = text_content.text.trim();\n if (!raw) {\n return undefined;\n }\n\n const truncated = raw.slice(0, 60);\n const at_boundary = truncated.replace(/\\s+\\S*$/, \"\");\n return (at_boundary || truncated).slice(0, 50);\n }\n\n #on_create_child = (child: Conversation) => {\n if (!this.#session_id) {\n return;\n }\n\n const child_session = new SessionManager(this.#store, child, {\n auto_save: this.#auto_save,\n auto_slug: !child.is_checkpoint && this.#auto_slug,\n });\n child_session.fork(this.#session_id, { detached: true }).catch((error) => {\n this.#event_emitter.emit(\"lifecycle_error\", { error, operation: \"fork\" });\n });\n this.#child_sessions.push(child_session);\n\n child.once(\"dispose\", () => {\n child_session[Symbol.dispose]();\n const idx = this.#child_sessions.indexOf(child_session);\n if (idx >= 0) {\n this.#child_sessions.splice(idx, 1);\n }\n });\n };\n\n #on_message_complete = () => {\n if (this.#session_id) {\n this.save().catch((error) => {\n this.#event_emitter.emit(\"lifecycle_error\", {\n error,\n operation: \"save\",\n context: { session_id: this.#session_id },\n });\n });\n }\n };\n\n #on_conversation_dispose = () => this[Symbol.dispose]();\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { Message } from \"@simulacra-ai/core\";\nimport type { SessionMetadata, SessionStore } from \"./types.ts\";\n\ninterface SessionFile {\n metadata: Omit<SessionMetadata, \"id\">;\n messages: Message[];\n}\n\n/**\n * A file-based implementation of SessionStore.\n *\n * This store persists sessions as JSON files in a specified directory. Each session\n * is stored in a separate file named with its session ID. The store also maintains\n * hard links for fork relationships to enable efficient querying of session trees.\n */\nexport class FileSessionStore implements SessionStore {\n readonly #root: string;\n\n /**\n * Creates a new FileSessionStore instance.\n *\n * @param root - The absolute path to the directory where session files will be stored.\n */\n constructor(root: string) {\n this.#root = root;\n }\n\n /**\n * Lists all sessions stored in the file system.\n *\n * Reads all JSON files from the root directory and parses their metadata.\n *\n * @returns A promise that resolves to an array of session metadata, sorted by most recently updated first.\n */\n async list(): Promise<SessionMetadata[]> {\n await this.#ensure_dir();\n const sessions: SessionMetadata[] = [];\n\n const entries = await fs.readdir(this.#root, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isFile() || !entry.name.endsWith(\".json\")) {\n continue;\n }\n const session = await this.#read_session(\n path.join(this.#root, entry.name),\n entry.name.replace(/\\.json$/, \"\"),\n );\n if (session) {\n sessions.push(session);\n }\n }\n\n return sessions.sort((a, b) => b.updated_at.localeCompare(a.updated_at));\n }\n\n /**\n * Loads a session from the file system.\n *\n * @param id - The unique identifier of the session to load.\n * @returns A promise that resolves to the session metadata and messages, or undefined if not found.\n */\n async load(id: string) {\n return this.#read_file(path.join(this.#root, `${id}.json`), id);\n }\n\n /**\n * Saves a session to the file system.\n *\n * Creates a new session file if the ID does not exist, or updates an existing file.\n * Automatically updates the updated_at timestamp and message_count. If the session\n * has a parent, creates a hard link in the parent's fork directory for efficient querying.\n *\n * @param id - The unique identifier of the session.\n * @param messages - The messages to store for this session.\n * @param metadata - Optional partial metadata to merge with existing metadata.\n * @returns A promise that resolves when the save operation is complete.\n */\n async save(id: string, messages: Message[], metadata?: Partial<SessionMetadata>) {\n const now = new Date().toISOString();\n await this.#ensure_dir();\n\n const file_path = path.join(this.#root, `${id}.json`);\n const existing = await this.#read_file(file_path, id);\n const existing_metadata: Partial<SessionMetadata> = existing?.metadata ?? {};\n const parent_id = metadata?.parent_id ?? existing_metadata.parent_id;\n\n const file: SessionFile = {\n metadata: {\n ...existing_metadata,\n created_at: existing_metadata.created_at ?? now,\n updated_at: now,\n message_count: messages.length,\n ...metadata,\n },\n messages,\n };\n\n await fs.writeFile(file_path, JSON.stringify(file, null, 2), \"utf8\");\n\n if (parent_id) {\n await this.#ensure_fork_link(parent_id, id);\n }\n }\n\n /**\n * Deletes a session from the file system.\n *\n * Removes the session file, any hard links in parent fork directories, and the session's\n * own fork directory if it exists.\n *\n * @param id - The unique identifier of the session to delete.\n * @returns A promise that resolves to true if the session was deleted, false if it was not found.\n */\n async delete(id: string) {\n const file_path = path.join(this.#root, `${id}.json`);\n try {\n const result = await this.#read_file(file_path, id);\n await fs.unlink(file_path);\n\n if (result?.metadata.parent_id) {\n const link = path.join(this.#root, `${result.metadata.parent_id}-forks`, `${id}.json`);\n try {\n await fs.unlink(link);\n } catch {\n /* link may not exist */\n }\n }\n\n const fork_dir = path.join(this.#root, `${id}-forks`);\n try {\n await fs.rm(fork_dir, { recursive: true });\n } catch {\n /* no forks */\n }\n\n return true;\n } catch {\n return false;\n }\n }\n\n async #ensure_fork_link(parent_id: string, fork_id: string) {\n const fork_dir = path.join(this.#root, `${parent_id}-forks`);\n await fs.mkdir(fork_dir, { recursive: true });\n\n const canonical = path.join(this.#root, `${fork_id}.json`);\n const link = path.join(fork_dir, `${fork_id}.json`);\n\n try {\n await fs.unlink(link);\n } catch {\n /* doesn't exist yet */\n }\n\n try {\n await fs.link(canonical, link);\n } catch {\n // hard links can fail across filesystems — fall back to no index\n }\n }\n\n async #read_file(file_path: string, id: string) {\n try {\n const content = await fs.readFile(file_path, \"utf8\");\n const data: SessionFile = JSON.parse(content);\n return {\n metadata: { id, ...data.metadata } as SessionMetadata,\n messages: data.messages,\n };\n } catch {\n return undefined;\n }\n }\n\n async #read_session(file_path: string, id: string): Promise<SessionMetadata | undefined> {\n try {\n const content = await fs.readFile(file_path, \"utf8\");\n const data: SessionFile = JSON.parse(content);\n return { id, ...data.metadata };\n } catch {\n return undefined;\n }\n }\n\n async #ensure_dir() {\n await fs.mkdir(this.#root, { recursive: true });\n }\n}\n","import type { Message } from \"@simulacra-ai/core\";\nimport type { SessionMetadata, SessionStore } from \"./types.ts\";\n\n/**\n * An in-memory implementation of SessionStore.\n *\n * This store keeps all session data in memory using a Map. Data is not persisted\n * across process restarts. Useful for testing or scenarios where persistence is not required.\n */\nexport class InMemorySessionStore implements SessionStore {\n readonly #sessions = new Map<string, { metadata: SessionMetadata; messages: Message[] }>();\n\n /**\n * Lists all sessions stored in memory.\n *\n * @returns A promise that resolves to an array of session metadata, sorted by most recently updated first.\n */\n async list(): Promise<SessionMetadata[]> {\n return [...this.#sessions.values()]\n .map((s) => s.metadata)\n .sort((a, b) => b.updated_at.localeCompare(a.updated_at));\n }\n\n /**\n * Loads a session from memory.\n *\n * Returns a deep clone of the stored data to prevent external mutations.\n *\n * @param id - The unique identifier of the session to load.\n * @returns A promise that resolves to the session metadata and messages, or undefined if not found.\n */\n async load(id: string) {\n const entry = this.#sessions.get(id);\n if (!entry) {\n return undefined;\n }\n return {\n metadata: { ...entry.metadata },\n messages: structuredClone(entry.messages),\n };\n }\n\n /**\n * Saves a session to memory.\n *\n * Creates a new session if the ID does not exist, or updates an existing session.\n * Automatically updates the updated_at timestamp and message_count.\n *\n * @param id - The unique identifier of the session.\n * @param messages - The messages to store for this session.\n * @param metadata - Optional partial metadata to merge with existing metadata.\n * @returns A promise that resolves when the save operation is complete.\n */\n async save(id: string, messages: Message[], metadata?: Partial<SessionMetadata>) {\n const now = new Date().toISOString();\n const existing = this.#sessions.get(id);\n\n this.#sessions.set(id, {\n metadata: {\n id,\n ...existing?.metadata,\n created_at: existing?.metadata.created_at ?? now,\n updated_at: now,\n message_count: messages.length,\n ...metadata,\n },\n messages: structuredClone(messages),\n });\n }\n\n /**\n * Deletes a session from memory.\n *\n * @param id - The unique identifier of the session to delete.\n * @returns A promise that resolves to true if the session was deleted, false if it was not found.\n */\n async delete(id: string) {\n return this.#sessions.delete(id);\n }\n}\n"],"mappings":"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","names":["EventEmitter","fs","path"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/session-manager.ts","../src/file-session-store.ts","../src/in-memory-session-store.ts","../src/drizzle-session-store.ts"],"sourcesContent":["export type {\n SessionMetadata,\n SessionStore,\n SessionManagerOptions,\n SessionManagerEvents,\n} from \"./types.ts\";\n\nexport { SessionManager } from \"./session-manager.ts\";\nexport { FileSessionStore } from \"./file-session-store.ts\";\nexport { InMemorySessionStore } from \"./in-memory-session-store.ts\";\nexport {\n DrizzleSessionStore,\n type DrizzleSessionAdapter,\n type DrizzleSessionRow,\n} from \"./drizzle-session-store.ts\";\n","import { randomUUID } from \"node:crypto\";\nimport EventEmitter from \"node:events\";\nimport type { Conversation, Message } from \"@simulacra-ai/core\";\nimport type {\n SessionMetadata,\n SessionStore,\n SessionManagerOptions,\n SessionManagerEvents,\n} from \"./types.ts\";\n\n/**\n * Manages conversation sessions including creation, loading, saving, and forking.\n *\n * The SessionManager acts as a bridge between a Conversation instance and a SessionStore,\n * handling session lifecycle, automatic saving, and session tree management through forking.\n * It supports disposable resource management and event emission for session operations.\n */\nexport class SessionManager {\n readonly #store: SessionStore;\n readonly #conversation: Conversation;\n readonly #auto_save: boolean;\n readonly #auto_slug: boolean;\n readonly #event_emitter = new EventEmitter<SessionManagerEvents>();\n readonly #child_sessions: SessionManager[] = [];\n\n #session_id?: string;\n #fork_offset = 0;\n #has_label = false;\n #disposed = false;\n\n /**\n * Creates a new SessionManager instance.\n *\n * @param store - The storage backend to use for persisting sessions.\n * @param conversation - The conversation instance to manage.\n * @param options - Optional configuration for session management behavior.\n */\n constructor(store: SessionStore, conversation: Conversation, options?: SessionManagerOptions) {\n this.#store = store;\n this.#conversation = conversation;\n this.#auto_save = options?.auto_save ?? true;\n this.#auto_slug = options?.auto_slug ?? true;\n\n if (this.#auto_save) {\n this.#conversation.on(\"message_complete\", this.#on_message_complete);\n }\n this.#conversation.on(\"create_child\", this.#on_create_child);\n this.#conversation.once(\"dispose\", this.#on_conversation_dispose);\n }\n\n /**\n * The ID of the currently active session.\n *\n * @returns The session ID if a session is active, otherwise undefined.\n */\n get current_session_id() {\n return this.#session_id;\n }\n\n /**\n * Whether a session is currently loaded.\n *\n * @returns True if a session is active, false otherwise.\n */\n get is_loaded() {\n return !!this.#session_id;\n }\n\n /**\n * Disposes of the SessionManager and cleans up resources.\n *\n * This method removes event listeners, disposes child sessions, and emits the dispose event.\n * It is called automatically when the associated conversation is disposed or when using\n * explicit resource management.\n */\n [Symbol.dispose]() {\n if (this.#disposed) {\n return;\n }\n this.#disposed = true;\n\n for (const child of this.#child_sessions) {\n child[Symbol.dispose]();\n }\n this.#child_sessions.length = 0;\n\n this.#conversation.off(\"message_complete\", this.#on_message_complete);\n this.#conversation.off(\"create_child\", this.#on_create_child);\n this.#conversation.off(\"dispose\", this.#on_conversation_dispose);\n this.#event_emitter.emit(\"dispose\");\n this.#event_emitter.removeAllListeners();\n }\n\n /**\n * Starts a new session with a freshly generated ID.\n *\n * This method clears the conversation history and creates a new session. If auto_save\n * is enabled or a label is provided, the session is immediately saved to the store.\n *\n * @param label - Optional label to assign to the new session.\n * @returns The generated session ID.\n */\n start_new(label?: string): string {\n this.#session_id = randomUUID();\n this.#fork_offset = 0;\n this.#has_label = !!label;\n if (this.#conversation.messages.length > 0) {\n this.#conversation.clear();\n }\n if (label || this.#auto_save) {\n this.save({ label }).catch((error) => {\n this.#event_emitter.emit(\"lifecycle_error\", {\n error,\n operation: \"save\",\n context: { session_id: this.#session_id },\n });\n });\n }\n return this.#session_id;\n }\n\n /**\n * Creates a new session as a fork of an existing parent session.\n *\n * A fork creates a new session that inherits messages from the parent session up to\n * the fork point. If detached, the fork does not inherit any messages from the parent\n * but still maintains a parent reference for organizational purposes.\n *\n * @param parent_session_id - The ID of the session to fork from.\n * @param options - Optional configuration for the fork operation.\n * @param options.detached - Whether to create a detached fork that does not inherit parent messages.\n * @returns A promise that resolves to the generated session ID for the fork.\n */\n async fork(parent_session_id: string, options?: { detached?: boolean }): Promise<string> {\n const detached = options?.detached ?? false;\n this.#session_id = randomUUID();\n this.#has_label = false;\n if (!detached) {\n this.#conversation.clear();\n }\n\n let fork_message_id: string | undefined;\n\n if (!detached) {\n const messages = await this.#resolve_messages(parent_session_id);\n if (messages.length > 0) {\n this.#conversation.load(messages);\n this.#fork_offset = messages.length;\n fork_message_id = messages.at(-1)?.id;\n } else {\n this.#fork_offset = 0;\n }\n } else {\n this.#fork_offset = 0;\n }\n\n const parent_result = await this.#store.load(parent_session_id);\n if (parent_result?.metadata.checkpoint_state) {\n this.#conversation.checkpoint_state = parent_result.metadata.checkpoint_state;\n }\n\n await this.save({\n parent_id: parent_session_id,\n fork_message_id,\n detached,\n });\n\n return this.#session_id;\n }\n\n /**\n * Loads a session by ID or loads the most recent session if no ID is provided.\n *\n * If no session ID is provided and no sessions exist in the store, this method\n * starts a new session automatically. Loading a session resolves its full message\n * history by recursively following parent references.\n *\n * @param id - The ID of the session to load, or undefined to load the most recent session.\n * @returns A promise that resolves when the session is loaded.\n * @throws {Error} If the specified session ID is not found in the store.\n */\n async load(id?: string): Promise<void> {\n if (id) {\n const messages = await this.#resolve_messages(id);\n const result = await this.#store.load(id);\n if (!result) {\n throw new Error(`session not found: ${id}`);\n }\n this.#session_id = id;\n this.#has_label = !!result.metadata.label;\n this.#conversation.clear();\n this.#fork_offset = messages.length - result.messages.length;\n this.#conversation.load(messages);\n this.#conversation.checkpoint_state = result.metadata.checkpoint_state;\n this.#emit_load(messages);\n return;\n }\n\n const sessions = await this.#store.list();\n if (!sessions.length) {\n this.start_new();\n return;\n }\n\n const latest = sessions[0];\n return this.load(latest.id);\n }\n\n /**\n * Saves the current session to the store.\n *\n * This method persists only the messages owned by this session, excluding any messages\n * inherited from parent sessions. If auto_slug is enabled and no label has been set,\n * a label is automatically generated from the first user message.\n *\n * @param metadata - Optional partial metadata to update on the session.\n * @returns A promise that resolves when the save operation is complete.\n * @throws {Error} If no session is currently active.\n */\n async save(\n metadata?: Partial<\n Pick<\n SessionMetadata,\n \"label\" | \"provider\" | \"model\" | \"parent_id\" | \"fork_message_id\" | \"detached\"\n >\n >,\n ) {\n if (!this.#session_id) {\n throw new Error(\"no active session\");\n }\n const all_messages = this.#conversation.messages;\n const owned_messages = [...all_messages].slice(this.#fork_offset);\n\n if (this.#auto_slug && !metadata?.label && !this.#has_label) {\n const slug = SessionManager.#derive_slug(all_messages);\n if (slug) {\n metadata = { ...metadata, label: slug };\n this.#has_label = true;\n }\n }\n\n const conversation_metadata: Partial<SessionMetadata> = { ...metadata };\n if (this.#conversation.is_checkpoint) {\n conversation_metadata.is_checkpoint = true;\n }\n const checkpoint_state = this.#conversation.checkpoint_state;\n if (checkpoint_state) {\n conversation_metadata.checkpoint_state = { ...checkpoint_state };\n }\n\n await this.#store.save(this.#session_id, owned_messages, conversation_metadata);\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this.#event_emitter as any).emit(\"save\", {\n id: this.#session_id,\n messages: Object.freeze([...all_messages]),\n });\n }\n\n /**\n * Lists all sessions stored in the store.\n *\n * @returns A promise that resolves to an array of session metadata, typically sorted by last update time.\n */\n async list() {\n return this.#store.list();\n }\n\n /**\n * Deletes a session from the store.\n *\n * If the deleted session is currently active, the conversation is cleared and the\n * current session is unset.\n *\n * @param id - The ID of the session to delete.\n * @returns A promise that resolves to true if the session was deleted, false if it was not found.\n */\n async delete(id: string) {\n if (id === this.#session_id) {\n this.#session_id = undefined;\n this.#conversation.clear();\n }\n return this.#store.delete(id);\n }\n\n /**\n * Renames a session by updating its label.\n *\n * @param id - The ID of the session to rename.\n * @param label - The new label to assign to the session.\n * @returns A promise that resolves when the rename operation is complete.\n * @throws {Error} If the specified session ID is not found in the store.\n */\n async rename(id: string, label: string) {\n const result = await this.#store.load(id);\n if (!result) {\n throw new Error(`session not found: ${id}`);\n }\n await this.#store.save(id, result.messages, { label });\n if (id === this.#session_id) {\n this.#has_label = true;\n }\n }\n\n /**\n * Registers an event listener for the specified event.\n *\n * @param event - The name of the event to listen for.\n * @param listener - The callback function to invoke when the event is emitted.\n * @returns This SessionManager instance for method chaining.\n */\n on<E extends keyof SessionManagerEvents>(\n event: E,\n listener: (...args: SessionManagerEvents[E]) => void,\n ): this {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this.#event_emitter as any).on(event, listener);\n return this;\n }\n\n /**\n * Removes an event listener for the specified event.\n *\n * @param event - The name of the event to stop listening for.\n * @param listener - The callback function to remove.\n * @returns This SessionManager instance for method chaining.\n */\n off<E extends keyof SessionManagerEvents>(\n event: E,\n listener: (...args: SessionManagerEvents[E]) => void,\n ): this {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this.#event_emitter as any).off(event, listener);\n return this;\n }\n\n #emit_load(messages: Readonly<Message[]>) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this.#event_emitter as any).emit(\"load\", {\n id: this.#session_id,\n messages: Object.freeze([...messages]),\n });\n }\n\n async #resolve_messages(id: string): Promise<Message[]> {\n const result = await this.#store.load(id);\n if (!result) {\n return [];\n }\n\n const { metadata, messages } = result;\n\n if (metadata.parent_id && !metadata.detached) {\n const parent_messages = await this.#resolve_messages(metadata.parent_id);\n if (metadata.fork_message_id) {\n const cut = parent_messages.findIndex((m) => m.id === metadata.fork_message_id);\n if (cut >= 0) {\n return [...parent_messages.slice(0, cut + 1), ...messages];\n }\n }\n return [...parent_messages, ...messages];\n }\n\n return messages;\n }\n\n static #derive_slug(messages: readonly Readonly<Message>[]): string | undefined {\n const first_user = messages.find((m) => m.role === \"user\");\n if (!first_user) {\n return undefined;\n }\n\n const text_content = first_user.content.find((c) => c.type === \"text\");\n if (!text_content || text_content.type !== \"text\" || !text_content.text) {\n return undefined;\n }\n\n const raw = text_content.text.trim();\n if (!raw) {\n return undefined;\n }\n\n const truncated = raw.slice(0, 60);\n const at_boundary = truncated.replace(/\\s+\\S*$/, \"\");\n return (at_boundary || truncated).slice(0, 50);\n }\n\n #on_create_child = (child: Conversation) => {\n if (!this.#session_id) {\n return;\n }\n\n const child_session = new SessionManager(this.#store, child, {\n auto_save: this.#auto_save,\n auto_slug: !child.is_checkpoint && this.#auto_slug,\n });\n child_session.fork(this.#session_id, { detached: true }).catch((error) => {\n this.#event_emitter.emit(\"lifecycle_error\", { error, operation: \"fork\" });\n });\n this.#child_sessions.push(child_session);\n\n child.once(\"dispose\", () => {\n child_session[Symbol.dispose]();\n const idx = this.#child_sessions.indexOf(child_session);\n if (idx >= 0) {\n this.#child_sessions.splice(idx, 1);\n }\n });\n };\n\n #on_message_complete = () => {\n if (this.#session_id) {\n this.save().catch((error) => {\n this.#event_emitter.emit(\"lifecycle_error\", {\n error,\n operation: \"save\",\n context: { session_id: this.#session_id },\n });\n });\n }\n };\n\n #on_conversation_dispose = () => this[Symbol.dispose]();\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { Message } from \"@simulacra-ai/core\";\nimport type { SessionMetadata, SessionStore } from \"./types.ts\";\n\ninterface SessionFile {\n metadata: Omit<SessionMetadata, \"id\">;\n messages: Message[];\n}\n\n/**\n * A file-based implementation of SessionStore.\n *\n * This store persists sessions as JSON files in a specified directory. Each session\n * is stored in a separate file named with its session ID. The store also maintains\n * hard links for fork relationships to enable efficient querying of session trees.\n */\nexport class FileSessionStore implements SessionStore {\n readonly #root: string;\n\n /**\n * Creates a new FileSessionStore instance.\n *\n * @param root - The absolute path to the directory where session files will be stored.\n */\n constructor(root: string) {\n this.#root = root;\n }\n\n /**\n * Lists all sessions stored in the file system.\n *\n * Reads all JSON files from the root directory and parses their metadata.\n *\n * @returns A promise that resolves to an array of session metadata, sorted by most recently updated first.\n */\n async list(): Promise<SessionMetadata[]> {\n await this.#ensure_dir();\n const sessions: SessionMetadata[] = [];\n\n const entries = await fs.readdir(this.#root, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isFile() || !entry.name.endsWith(\".json\")) {\n continue;\n }\n const session = await this.#read_session(\n path.join(this.#root, entry.name),\n entry.name.replace(/\\.json$/, \"\"),\n );\n if (session) {\n sessions.push(session);\n }\n }\n\n return sessions.sort((a, b) => b.updated_at.localeCompare(a.updated_at));\n }\n\n /**\n * Loads a session from the file system.\n *\n * @param id - The unique identifier of the session to load.\n * @returns A promise that resolves to the session metadata and messages, or undefined if not found.\n */\n async load(id: string) {\n return this.#read_file(path.join(this.#root, `${id}.json`), id);\n }\n\n /**\n * Saves a session to the file system.\n *\n * Creates a new session file if the ID does not exist, or updates an existing file.\n * Automatically updates the updated_at timestamp and message_count. If the session\n * has a parent, creates a hard link in the parent's fork directory for efficient querying.\n *\n * @param id - The unique identifier of the session.\n * @param messages - The messages to store for this session.\n * @param metadata - Optional partial metadata to merge with existing metadata.\n * @returns A promise that resolves when the save operation is complete.\n */\n async save(id: string, messages: Message[], metadata?: Partial<SessionMetadata>) {\n const now = new Date().toISOString();\n await this.#ensure_dir();\n\n const file_path = path.join(this.#root, `${id}.json`);\n const existing = await this.#read_file(file_path, id);\n const existing_metadata: Partial<SessionMetadata> = existing?.metadata ?? {};\n const parent_id = metadata?.parent_id ?? existing_metadata.parent_id;\n\n const file: SessionFile = {\n metadata: {\n ...existing_metadata,\n created_at: existing_metadata.created_at ?? now,\n updated_at: now,\n message_count: messages.length,\n ...metadata,\n },\n messages,\n };\n\n await fs.writeFile(file_path, JSON.stringify(file, null, 2), \"utf8\");\n\n if (parent_id) {\n await this.#ensure_fork_link(parent_id, id);\n }\n }\n\n /**\n * Deletes a session from the file system.\n *\n * Removes the session file, any hard links in parent fork directories, and the session's\n * own fork directory if it exists.\n *\n * @param id - The unique identifier of the session to delete.\n * @returns A promise that resolves to true if the session was deleted, false if it was not found.\n */\n async delete(id: string) {\n const file_path = path.join(this.#root, `${id}.json`);\n try {\n const result = await this.#read_file(file_path, id);\n await fs.unlink(file_path);\n\n if (result?.metadata.parent_id) {\n const link = path.join(this.#root, `${result.metadata.parent_id}-forks`, `${id}.json`);\n try {\n await fs.unlink(link);\n } catch {\n /* link may not exist */\n }\n }\n\n const fork_dir = path.join(this.#root, `${id}-forks`);\n try {\n await fs.rm(fork_dir, { recursive: true });\n } catch {\n /* no forks */\n }\n\n return true;\n } catch {\n return false;\n }\n }\n\n async #ensure_fork_link(parent_id: string, fork_id: string) {\n const fork_dir = path.join(this.#root, `${parent_id}-forks`);\n await fs.mkdir(fork_dir, { recursive: true });\n\n const canonical = path.join(this.#root, `${fork_id}.json`);\n const link = path.join(fork_dir, `${fork_id}.json`);\n\n try {\n await fs.unlink(link);\n } catch {\n /* doesn't exist yet */\n }\n\n try {\n await fs.link(canonical, link);\n } catch {\n // hard links can fail across filesystems — fall back to no index\n }\n }\n\n async #read_file(file_path: string, id: string) {\n try {\n const content = await fs.readFile(file_path, \"utf8\");\n const data: SessionFile = JSON.parse(content);\n return {\n metadata: { id, ...data.metadata } as SessionMetadata,\n messages: data.messages,\n };\n } catch {\n return undefined;\n }\n }\n\n async #read_session(file_path: string, id: string): Promise<SessionMetadata | undefined> {\n try {\n const content = await fs.readFile(file_path, \"utf8\");\n const data: SessionFile = JSON.parse(content);\n return { id, ...data.metadata };\n } catch {\n return undefined;\n }\n }\n\n async #ensure_dir() {\n await fs.mkdir(this.#root, { recursive: true });\n }\n}\n","import type { Message } from \"@simulacra-ai/core\";\nimport type { SessionMetadata, SessionStore } from \"./types.ts\";\n\n/**\n * An in-memory implementation of SessionStore.\n *\n * This store keeps all session data in memory using a Map. Data is not persisted\n * across process restarts. Useful for testing or scenarios where persistence is not required.\n */\nexport class InMemorySessionStore implements SessionStore {\n readonly #sessions = new Map<string, { metadata: SessionMetadata; messages: Message[] }>();\n\n /**\n * Lists all sessions stored in memory.\n *\n * @returns A promise that resolves to an array of session metadata, sorted by most recently updated first.\n */\n async list(): Promise<SessionMetadata[]> {\n return [...this.#sessions.values()]\n .map((s) => s.metadata)\n .sort((a, b) => b.updated_at.localeCompare(a.updated_at));\n }\n\n /**\n * Loads a session from memory.\n *\n * Returns a deep clone of the stored data to prevent external mutations.\n *\n * @param id - The unique identifier of the session to load.\n * @returns A promise that resolves to the session metadata and messages, or undefined if not found.\n */\n async load(id: string) {\n const entry = this.#sessions.get(id);\n if (!entry) {\n return undefined;\n }\n return {\n metadata: { ...entry.metadata },\n messages: structuredClone(entry.messages),\n };\n }\n\n /**\n * Saves a session to memory.\n *\n * Creates a new session if the ID does not exist, or updates an existing session.\n * Automatically updates the updated_at timestamp and message_count.\n *\n * @param id - The unique identifier of the session.\n * @param messages - The messages to store for this session.\n * @param metadata - Optional partial metadata to merge with existing metadata.\n * @returns A promise that resolves when the save operation is complete.\n */\n async save(id: string, messages: Message[], metadata?: Partial<SessionMetadata>) {\n const now = new Date().toISOString();\n const existing = this.#sessions.get(id);\n\n this.#sessions.set(id, {\n metadata: {\n id,\n ...existing?.metadata,\n created_at: existing?.metadata.created_at ?? now,\n updated_at: now,\n message_count: messages.length,\n ...metadata,\n },\n messages: structuredClone(messages),\n });\n }\n\n /**\n * Deletes a session from memory.\n *\n * @param id - The unique identifier of the session to delete.\n * @returns A promise that resolves to true if the session was deleted, false if it was not found.\n */\n async delete(id: string) {\n return this.#sessions.delete(id);\n }\n}\n","import type { Message } from \"@simulacra-ai/core\";\nimport type { SessionMetadata, SessionStore } from \"./types.ts\";\n\n/**\n * The shape of a row in the sessions table.\n *\n * Use this as a reference when defining your drizzle table schema. The `metadata`\n * and `messages` columns should be JSON/JSONB columns (or text with JSON mode in SQLite).\n *\n * Example drizzle schema (PostgreSQL):\n * ```ts\n * import { pgTable, text, jsonb } from \"drizzle-orm/pg-core\";\n *\n * export const sessionsTable = pgTable(\"sessions\", {\n * id: text(\"id\").primaryKey(),\n * metadata: jsonb(\"metadata\").notNull(),\n * messages: jsonb(\"messages\").notNull(),\n * updated_at: text(\"updated_at\").notNull(),\n * });\n * ```\n *\n * Example drizzle schema (SQLite):\n * ```ts\n * import { sqliteTable, text } from \"drizzle-orm/sqlite-core\";\n *\n * export const sessionsTable = sqliteTable(\"sessions\", {\n * id: text(\"id\").primaryKey(),\n * metadata: text(\"metadata\", { mode: \"json\" }).notNull(),\n * messages: text(\"messages\", { mode: \"json\" }).notNull(),\n * updated_at: text(\"updated_at\").notNull(),\n * });\n * ```\n */\nexport type DrizzleSessionRow = {\n id: string;\n /** JSON-serializable value storing `Omit<SessionMetadata, \"id\">`. */\n metadata: unknown;\n /** JSON-serializable value storing `Message[]`. */\n messages: unknown;\n /** ISO 8601 timestamp — denormalized from metadata for efficient ORDER BY. */\n updated_at: string;\n}\n\n/**\n * Adapter interface that bridges `DrizzleSessionStore` with a drizzle database instance.\n *\n * Implement this interface using your own drizzle `db` and table, then pass it to\n * `DrizzleSessionStore`. This keeps drizzle out of the session package's dependencies.\n *\n * @example\n * ```ts\n * import { eq, desc } from \"drizzle-orm\";\n * import { DrizzleSessionStore, type DrizzleSessionAdapter } from \"@simulacra-ai/session\";\n *\n * const adapter: DrizzleSessionAdapter = {\n * list: () =>\n * db.select().from(sessionsTable).orderBy(desc(sessionsTable.updated_at)),\n *\n * load: async (id) => {\n * const [row] = await db\n * .select({ metadata: sessionsTable.metadata, messages: sessionsTable.messages })\n * .from(sessionsTable)\n * .where(eq(sessionsTable.id, id));\n * return row;\n * },\n *\n * upsert: (row) =>\n * db\n * .insert(sessionsTable)\n * .values(row)\n * .onConflictDoUpdate({\n * target: sessionsTable.id,\n * set: { metadata: row.metadata, messages: row.messages, updated_at: row.updated_at },\n * }),\n *\n * delete: async (id) => {\n * const result = await db.delete(sessionsTable).where(eq(sessionsTable.id, id));\n * return result.rowCount > 0;\n * },\n * };\n *\n * const store = new DrizzleSessionStore(adapter);\n * ```\n */\nexport type DrizzleSessionAdapter = {\n /**\n * Returns all session rows, sorted by `updated_at` descending (most recent first).\n */\n list(): Promise<DrizzleSessionRow[]>;\n\n /**\n * Returns the metadata and messages for a single session, or undefined if not found.\n *\n * Only `metadata` and `messages` are required in the return value.\n */\n load(id: string): Promise<Pick<DrizzleSessionRow, \"metadata\" | \"messages\"> | undefined>;\n\n /**\n * Inserts or updates a session row. On conflict with an existing `id`, the\n * metadata, messages, and updated_at columns should be updated.\n */\n upsert(row: DrizzleSessionRow): Promise<void>;\n\n /**\n * Deletes a session by id.\n *\n * @returns `true` if a row was deleted, `false` if no row with that id existed.\n */\n delete(id: string): Promise<boolean>;\n}\n\n/**\n * A database-backed implementation of `SessionStore` powered by drizzle ORM.\n *\n * Rather than importing drizzle directly, this store accepts a {@link DrizzleSessionAdapter}\n * that you implement using your own drizzle instance and table. This keeps drizzle out of\n * this package's dependencies while still providing full session persistence.\n *\n * See {@link DrizzleSessionAdapter} for an implementation example and\n * {@link DrizzleSessionRow} for the expected table schema.\n */\nexport class DrizzleSessionStore implements SessionStore {\n readonly #adapter: DrizzleSessionAdapter;\n\n constructor(adapter: DrizzleSessionAdapter) {\n this.#adapter = adapter;\n }\n\n /**\n * Lists all sessions, sorted by most recently updated first.\n *\n * @returns A promise that resolves to an array of session metadata.\n */\n async list(): Promise<SessionMetadata[]> {\n const rows = await this.#adapter.list();\n return rows.map((row) => ({\n id: row.id,\n ...(row.metadata as Omit<SessionMetadata, \"id\">),\n }));\n }\n\n /**\n * Loads a session by its ID.\n *\n * @param id - The unique identifier of the session to load.\n * @returns A promise that resolves to the session metadata and messages, or undefined if not found.\n */\n async load(id: string): Promise<{ metadata: SessionMetadata; messages: Message[] } | undefined> {\n const row = await this.#adapter.load(id);\n if (!row) {\n return undefined;\n }\n return {\n metadata: {\n id,\n ...(row.metadata as Omit<SessionMetadata, \"id\">),\n },\n messages: row.messages as Message[],\n };\n }\n\n /**\n * Saves a session with the given messages and metadata.\n *\n * Creates a new session if the ID does not exist, or updates the existing session.\n * Automatically updates the `updated_at` timestamp and `message_count`.\n *\n * @param id - The unique identifier of the session.\n * @param messages - The messages to store for this session.\n * @param metadata - Optional partial metadata to merge with existing metadata.\n * @returns A promise that resolves when the save operation is complete.\n */\n async save(id: string, messages: Message[], metadata?: Partial<SessionMetadata>): Promise<void> {\n const now = new Date().toISOString();\n const existing = await this.#adapter.load(id);\n const existing_metadata = existing?.metadata as Partial<SessionMetadata> | undefined;\n\n const merged: Omit<SessionMetadata, \"id\"> = {\n ...existing_metadata,\n created_at: existing_metadata?.created_at ?? now,\n updated_at: now,\n message_count: messages.length,\n ...metadata,\n };\n\n await this.#adapter.upsert({\n id,\n metadata: merged,\n messages,\n updated_at: now,\n });\n }\n\n /**\n * Deletes a session by its ID.\n *\n * @param id - The unique identifier of the session to delete.\n * @returns A promise that resolves to true if the session was deleted, false if it was not found.\n */\n async delete(id: string): Promise<boolean> {\n return this.#adapter.delete(id);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,yBAA2B;AAC3B,yBAAyB;AAgBlB,IAAM,iBAAN,MAAM,gBAAe;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,iBAAiB,IAAI,mBAAAA,QAAmC;AAAA,EACxD,kBAAoC,CAAC;AAAA,EAE9C;AAAA,EACA,eAAe;AAAA,EACf,aAAa;AAAA,EACb,YAAY;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASZ,YAAY,OAAqB,cAA4B,SAAiC;AAC5F,SAAK,SAAS;AACd,SAAK,gBAAgB;AACrB,SAAK,aAAa,SAAS,aAAa;AACxC,SAAK,aAAa,SAAS,aAAa;AAExC,QAAI,KAAK,YAAY;AACnB,WAAK,cAAc,GAAG,oBAAoB,KAAK,oBAAoB;AAAA,IACrE;AACA,SAAK,cAAc,GAAG,gBAAgB,KAAK,gBAAgB;AAC3D,SAAK,cAAc,KAAK,WAAW,KAAK,wBAAwB;AAAA,EAClE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,IAAI,qBAAqB;AACvB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,IAAI,YAAY;AACd,WAAO,CAAC,CAAC,KAAK;AAAA,EAChB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,CAAC,OAAO,OAAO,IAAI;AACjB,QAAI,KAAK,WAAW;AAClB;AAAA,IACF;AACA,SAAK,YAAY;AAEjB,eAAW,SAAS,KAAK,iBAAiB;AACxC,YAAM,OAAO,OAAO,EAAE;AAAA,IACxB;AACA,SAAK,gBAAgB,SAAS;AAE9B,SAAK,cAAc,IAAI,oBAAoB,KAAK,oBAAoB;AACpE,SAAK,cAAc,IAAI,gBAAgB,KAAK,gBAAgB;AAC5D,SAAK,cAAc,IAAI,WAAW,KAAK,wBAAwB;AAC/D,SAAK,eAAe,KAAK,SAAS;AAClC,SAAK,eAAe,mBAAmB;AAAA,EACzC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,UAAU,OAAwB;AAChC,SAAK,kBAAc,+BAAW;AAC9B,SAAK,eAAe;AACpB,SAAK,aAAa,CAAC,CAAC;AACpB,QAAI,KAAK,cAAc,SAAS,SAAS,GAAG;AAC1C,WAAK,cAAc,MAAM;AAAA,IAC3B;AACA,QAAI,SAAS,KAAK,YAAY;AAC5B,WAAK,KAAK,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,UAAU;AACpC,aAAK,eAAe,KAAK,mBAAmB;AAAA,UAC1C;AAAA,UACA,WAAW;AAAA,UACX,SAAS,EAAE,YAAY,KAAK,YAAY;AAAA,QAC1C,CAAC;AAAA,MACH,CAAC;AAAA,IACH;AACA,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,MAAM,KAAK,mBAA2B,SAAmD;AACvF,UAAM,WAAW,SAAS,YAAY;AACtC,SAAK,kBAAc,+BAAW;AAC9B,SAAK,aAAa;AAClB,QAAI,CAAC,UAAU;AACb,WAAK,cAAc,MAAM;AAAA,IAC3B;AAEA,QAAI;AAEJ,QAAI,CAAC,UAAU;AACb,YAAM,WAAW,MAAM,KAAK,kBAAkB,iBAAiB;AAC/D,UAAI,SAAS,SAAS,GAAG;AACvB,aAAK,cAAc,KAAK,QAAQ;AAChC,aAAK,eAAe,SAAS;AAC7B,0BAAkB,SAAS,GAAG,EAAE,GAAG;AAAA,MACrC,OAAO;AACL,aAAK,eAAe;AAAA,MACtB;AAAA,IACF,OAAO;AACL,WAAK,eAAe;AAAA,IACtB;AAEA,UAAM,gBAAgB,MAAM,KAAK,OAAO,KAAK,iBAAiB;AAC9D,QAAI,eAAe,SAAS,kBAAkB;AAC5C,WAAK,cAAc,mBAAmB,cAAc,SAAS;AAAA,IAC/D;AAEA,UAAM,KAAK,KAAK;AAAA,MACd,WAAW;AAAA,MACX;AAAA,MACA;AAAA,IACF,CAAC;AAED,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAM,KAAK,IAA4B;AACrC,QAAI,IAAI;AACN,YAAM,WAAW,MAAM,KAAK,kBAAkB,EAAE;AAChD,YAAM,SAAS,MAAM,KAAK,OAAO,KAAK,EAAE;AACxC,UAAI,CAAC,QAAQ;AACX,cAAM,IAAI,MAAM,sBAAsB,EAAE,EAAE;AAAA,MAC5C;AACA,WAAK,cAAc;AACnB,WAAK,aAAa,CAAC,CAAC,OAAO,SAAS;AACpC,WAAK,cAAc,MAAM;AACzB,WAAK,eAAe,SAAS,SAAS,OAAO,SAAS;AACtD,WAAK,cAAc,KAAK,QAAQ;AAChC,WAAK,cAAc,mBAAmB,OAAO,SAAS;AACtD,WAAK,WAAW,QAAQ;AACxB;AAAA,IACF;AAEA,UAAM,WAAW,MAAM,KAAK,OAAO,KAAK;AACxC,QAAI,CAAC,SAAS,QAAQ;AACpB,WAAK,UAAU;AACf;AAAA,IACF;AAEA,UAAM,SAAS,SAAS,CAAC;AACzB,WAAO,KAAK,KAAK,OAAO,EAAE;AAAA,EAC5B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAM,KACJ,UAMA;AACA,QAAI,CAAC,KAAK,aAAa;AACrB,YAAM,IAAI,MAAM,mBAAmB;AAAA,IACrC;AACA,UAAM,eAAe,KAAK,cAAc;AACxC,UAAM,iBAAiB,CAAC,GAAG,YAAY,EAAE,MAAM,KAAK,YAAY;AAEhE,QAAI,KAAK,cAAc,CAAC,UAAU,SAAS,CAAC,KAAK,YAAY;AAC3D,YAAM,OAAO,gBAAe,aAAa,YAAY;AACrD,UAAI,MAAM;AACR,mBAAW,EAAE,GAAG,UAAU,OAAO,KAAK;AACtC,aAAK,aAAa;AAAA,MACpB;AAAA,IACF;AAEA,UAAM,wBAAkD,EAAE,GAAG,SAAS;AACtE,QAAI,KAAK,cAAc,eAAe;AACpC,4BAAsB,gBAAgB;AAAA,IACxC;AACA,UAAM,mBAAmB,KAAK,cAAc;AAC5C,QAAI,kBAAkB;AACpB,4BAAsB,mBAAmB,EAAE,GAAG,iBAAiB;AAAA,IACjE;AAEA,UAAM,KAAK,OAAO,KAAK,KAAK,aAAa,gBAAgB,qBAAqB;AAG9E,IAAC,KAAK,eAAuB,KAAK,QAAQ;AAAA,MACxC,IAAI,KAAK;AAAA,MACT,UAAU,OAAO,OAAO,CAAC,GAAG,YAAY,CAAC;AAAA,IAC3C,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,OAAO;AACX,WAAO,KAAK,OAAO,KAAK;AAAA,EAC1B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,OAAO,IAAY;AACvB,QAAI,OAAO,KAAK,aAAa;AAC3B,WAAK,cAAc;AACnB,WAAK,cAAc,MAAM;AAAA,IAC3B;AACA,WAAO,KAAK,OAAO,OAAO,EAAE;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,OAAO,IAAY,OAAe;AACtC,UAAM,SAAS,MAAM,KAAK,OAAO,KAAK,EAAE;AACxC,QAAI,CAAC,QAAQ;AACX,YAAM,IAAI,MAAM,sBAAsB,EAAE,EAAE;AAAA,IAC5C;AACA,UAAM,KAAK,OAAO,KAAK,IAAI,OAAO,UAAU,EAAE,MAAM,CAAC;AACrD,QAAI,OAAO,KAAK,aAAa;AAC3B,WAAK,aAAa;AAAA,IACpB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,GACE,OACA,UACM;AAEN,IAAC,KAAK,eAAuB,GAAG,OAAO,QAAQ;AAC/C,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,IACE,OACA,UACM;AAEN,IAAC,KAAK,eAAuB,IAAI,OAAO,QAAQ;AAChD,WAAO;AAAA,EACT;AAAA,EAEA,WAAW,UAA+B;AAExC,IAAC,KAAK,eAAuB,KAAK,QAAQ;AAAA,MACxC,IAAI,KAAK;AAAA,MACT,UAAU,OAAO,OAAO,CAAC,GAAG,QAAQ,CAAC;AAAA,IACvC,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,kBAAkB,IAAgC;AACtD,UAAM,SAAS,MAAM,KAAK,OAAO,KAAK,EAAE;AACxC,QAAI,CAAC,QAAQ;AACX,aAAO,CAAC;AAAA,IACV;AAEA,UAAM,EAAE,UAAU,SAAS,IAAI;AAE/B,QAAI,SAAS,aAAa,CAAC,SAAS,UAAU;AAC5C,YAAM,kBAAkB,MAAM,KAAK,kBAAkB,SAAS,SAAS;AACvE,UAAI,SAAS,iBAAiB;AAC5B,cAAM,MAAM,gBAAgB,UAAU,CAAC,MAAM,EAAE,OAAO,SAAS,eAAe;AAC9E,YAAI,OAAO,GAAG;AACZ,iBAAO,CAAC,GAAG,gBAAgB,MAAM,GAAG,MAAM,CAAC,GAAG,GAAG,QAAQ;AAAA,QAC3D;AAAA,MACF;AACA,aAAO,CAAC,GAAG,iBAAiB,GAAG,QAAQ;AAAA,IACzC;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,OAAO,aAAa,UAA4D;AAC9E,UAAM,aAAa,SAAS,KAAK,CAAC,MAAM,EAAE,SAAS,MAAM;AACzD,QAAI,CAAC,YAAY;AACf,aAAO;AAAA,IACT;AAEA,UAAM,eAAe,WAAW,QAAQ,KAAK,CAAC,MAAM,EAAE,SAAS,MAAM;AACrE,QAAI,CAAC,gBAAgB,aAAa,SAAS,UAAU,CAAC,aAAa,MAAM;AACvE,aAAO;AAAA,IACT;AAEA,UAAM,MAAM,aAAa,KAAK,KAAK;AACnC,QAAI,CAAC,KAAK;AACR,aAAO;AAAA,IACT;AAEA,UAAM,YAAY,IAAI,MAAM,GAAG,EAAE;AACjC,UAAM,cAAc,UAAU,QAAQ,WAAW,EAAE;AACnD,YAAQ,eAAe,WAAW,MAAM,GAAG,EAAE;AAAA,EAC/C;AAAA,EAEA,mBAAmB,CAAC,UAAwB;AAC1C,QAAI,CAAC,KAAK,aAAa;AACrB;AAAA,IACF;AAEA,UAAM,gBAAgB,IAAI,gBAAe,KAAK,QAAQ,OAAO;AAAA,MAC3D,WAAW,KAAK;AAAA,MAChB,WAAW,CAAC,MAAM,iBAAiB,KAAK;AAAA,IAC1C,CAAC;AACD,kBAAc,KAAK,KAAK,aAAa,EAAE,UAAU,KAAK,CAAC,EAAE,MAAM,CAAC,UAAU;AACxE,WAAK,eAAe,KAAK,mBAAmB,EAAE,OAAO,WAAW,OAAO,CAAC;AAAA,IAC1E,CAAC;AACD,SAAK,gBAAgB,KAAK,aAAa;AAEvC,UAAM,KAAK,WAAW,MAAM;AAC1B,oBAAc,OAAO,OAAO,EAAE;AAC9B,YAAM,MAAM,KAAK,gBAAgB,QAAQ,aAAa;AACtD,UAAI,OAAO,GAAG;AACZ,aAAK,gBAAgB,OAAO,KAAK,CAAC;AAAA,MACpC;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEA,uBAAuB,MAAM;AAC3B,QAAI,KAAK,aAAa;AACpB,WAAK,KAAK,EAAE,MAAM,CAAC,UAAU;AAC3B,aAAK,eAAe,KAAK,mBAAmB;AAAA,UAC1C;AAAA,UACA,WAAW;AAAA,UACX,SAAS,EAAE,YAAY,KAAK,YAAY;AAAA,QAC1C,CAAC;AAAA,MACH,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,2BAA2B,MAAM,KAAK,OAAO,OAAO,EAAE;AACxD;;;ACvaA,sBAAe;AACf,uBAAiB;AAgBV,IAAM,mBAAN,MAA+C;AAAA,EAC3C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOT,YAAY,MAAc;AACxB,SAAK,QAAQ;AAAA,EACf;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,MAAM,OAAmC;AACvC,UAAM,KAAK,YAAY;AACvB,UAAM,WAA8B,CAAC;AAErC,UAAM,UAAU,MAAM,gBAAAC,QAAG,QAAQ,KAAK,OAAO,EAAE,eAAe,KAAK,CAAC;AACpE,eAAW,SAAS,SAAS;AAC3B,UAAI,CAAC,MAAM,OAAO,KAAK,CAAC,MAAM,KAAK,SAAS,OAAO,GAAG;AACpD;AAAA,MACF;AACA,YAAM,UAAU,MAAM,KAAK;AAAA,QACzB,iBAAAC,QAAK,KAAK,KAAK,OAAO,MAAM,IAAI;AAAA,QAChC,MAAM,KAAK,QAAQ,WAAW,EAAE;AAAA,MAClC;AACA,UAAI,SAAS;AACX,iBAAS,KAAK,OAAO;AAAA,MACvB;AAAA,IACF;AAEA,WAAO,SAAS,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,cAAc,EAAE,UAAU,CAAC;AAAA,EACzE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,KAAK,IAAY;AACrB,WAAO,KAAK,WAAW,iBAAAA,QAAK,KAAK,KAAK,OAAO,GAAG,EAAE,OAAO,GAAG,EAAE;AAAA,EAChE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAcA,MAAM,KAAK,IAAY,UAAqB,UAAqC;AAC/E,UAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AACnC,UAAM,KAAK,YAAY;AAEvB,UAAM,YAAY,iBAAAA,QAAK,KAAK,KAAK,OAAO,GAAG,EAAE,OAAO;AACpD,UAAM,WAAW,MAAM,KAAK,WAAW,WAAW,EAAE;AACpD,UAAM,oBAA8C,UAAU,YAAY,CAAC;AAC3E,UAAM,YAAY,UAAU,aAAa,kBAAkB;AAE3D,UAAM,OAAoB;AAAA,MACxB,UAAU;AAAA,QACR,GAAG;AAAA,QACH,YAAY,kBAAkB,cAAc;AAAA,QAC5C,YAAY;AAAA,QACZ,eAAe,SAAS;AAAA,QACxB,GAAG;AAAA,MACL;AAAA,MACA;AAAA,IACF;AAEA,UAAM,gBAAAD,QAAG,UAAU,WAAW,KAAK,UAAU,MAAM,MAAM,CAAC,GAAG,MAAM;AAEnE,QAAI,WAAW;AACb,YAAM,KAAK,kBAAkB,WAAW,EAAE;AAAA,IAC5C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAWA,MAAM,OAAO,IAAY;AACvB,UAAM,YAAY,iBAAAC,QAAK,KAAK,KAAK,OAAO,GAAG,EAAE,OAAO;AACpD,QAAI;AACF,YAAM,SAAS,MAAM,KAAK,WAAW,WAAW,EAAE;AAClD,YAAM,gBAAAD,QAAG,OAAO,SAAS;AAEzB,UAAI,QAAQ,SAAS,WAAW;AAC9B,cAAM,OAAO,iBAAAC,QAAK,KAAK,KAAK,OAAO,GAAG,OAAO,SAAS,SAAS,UAAU,GAAG,EAAE,OAAO;AACrF,YAAI;AACF,gBAAM,gBAAAD,QAAG,OAAO,IAAI;AAAA,QACtB,QAAQ;AAAA,QAER;AAAA,MACF;AAEA,YAAM,WAAW,iBAAAC,QAAK,KAAK,KAAK,OAAO,GAAG,EAAE,QAAQ;AACpD,UAAI;AACF,cAAM,gBAAAD,QAAG,GAAG,UAAU,EAAE,WAAW,KAAK,CAAC;AAAA,MAC3C,QAAQ;AAAA,MAER;AAEA,aAAO;AAAA,IACT,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,kBAAkB,WAAmB,SAAiB;AAC1D,UAAM,WAAW,iBAAAC,QAAK,KAAK,KAAK,OAAO,GAAG,SAAS,QAAQ;AAC3D,UAAM,gBAAAD,QAAG,MAAM,UAAU,EAAE,WAAW,KAAK,CAAC;AAE5C,UAAM,YAAY,iBAAAC,QAAK,KAAK,KAAK,OAAO,GAAG,OAAO,OAAO;AACzD,UAAM,OAAO,iBAAAA,QAAK,KAAK,UAAU,GAAG,OAAO,OAAO;AAElD,QAAI;AACF,YAAM,gBAAAD,QAAG,OAAO,IAAI;AAAA,IACtB,QAAQ;AAAA,IAER;AAEA,QAAI;AACF,YAAM,gBAAAA,QAAG,KAAK,WAAW,IAAI;AAAA,IAC/B,QAAQ;AAAA,IAER;AAAA,EACF;AAAA,EAEA,MAAM,WAAW,WAAmB,IAAY;AAC9C,QAAI;AACF,YAAM,UAAU,MAAM,gBAAAA,QAAG,SAAS,WAAW,MAAM;AACnD,YAAM,OAAoB,KAAK,MAAM,OAAO;AAC5C,aAAO;AAAA,QACL,UAAU,EAAE,IAAI,GAAG,KAAK,SAAS;AAAA,QACjC,UAAU,KAAK;AAAA,MACjB;AAAA,IACF,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,cAAc,WAAmB,IAAkD;AACvF,QAAI;AACF,YAAM,UAAU,MAAM,gBAAAA,QAAG,SAAS,WAAW,MAAM;AACnD,YAAM,OAAoB,KAAK,MAAM,OAAO;AAC5C,aAAO,EAAE,IAAI,GAAG,KAAK,SAAS;AAAA,IAChC,QAAQ;AACN,aAAO;AAAA,IACT;AAAA,EACF;AAAA,EAEA,MAAM,cAAc;AAClB,UAAM,gBAAAA,QAAG,MAAM,KAAK,OAAO,EAAE,WAAW,KAAK,CAAC;AAAA,EAChD;AACF;;;ACpLO,IAAM,uBAAN,MAAmD;AAAA,EAC/C,YAAY,oBAAI,IAAgE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOzF,MAAM,OAAmC;AACvC,WAAO,CAAC,GAAG,KAAK,UAAU,OAAO,CAAC,EAC/B,IAAI,CAAC,MAAM,EAAE,QAAQ,EACrB,KAAK,CAAC,GAAG,MAAM,EAAE,WAAW,cAAc,EAAE,UAAU,CAAC;AAAA,EAC5D;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUA,MAAM,KAAK,IAAY;AACrB,UAAM,QAAQ,KAAK,UAAU,IAAI,EAAE;AACnC,QAAI,CAAC,OAAO;AACV,aAAO;AAAA,IACT;AACA,WAAO;AAAA,MACL,UAAU,EAAE,GAAG,MAAM,SAAS;AAAA,MAC9B,UAAU,gBAAgB,MAAM,QAAQ;AAAA,IAC1C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAM,KAAK,IAAY,UAAqB,UAAqC;AAC/E,UAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AACnC,UAAM,WAAW,KAAK,UAAU,IAAI,EAAE;AAEtC,SAAK,UAAU,IAAI,IAAI;AAAA,MACrB,UAAU;AAAA,QACR;AAAA,QACA,GAAG,UAAU;AAAA,QACb,YAAY,UAAU,SAAS,cAAc;AAAA,QAC7C,YAAY;AAAA,QACZ,eAAe,SAAS;AAAA,QACxB,GAAG;AAAA,MACL;AAAA,MACA,UAAU,gBAAgB,QAAQ;AAAA,IACpC,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,OAAO,IAAY;AACvB,WAAO,KAAK,UAAU,OAAO,EAAE;AAAA,EACjC;AACF;;;AC0CO,IAAM,sBAAN,MAAkD;AAAA,EAC9C;AAAA,EAET,YAAY,SAAgC;AAC1C,SAAK,WAAW;AAAA,EAClB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,OAAmC;AACvC,UAAM,OAAO,MAAM,KAAK,SAAS,KAAK;AACtC,WAAO,KAAK,IAAI,CAAC,SAAS;AAAA,MACxB,IAAI,IAAI;AAAA,MACR,GAAI,IAAI;AAAA,IACV,EAAE;AAAA,EACJ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,KAAK,IAAqF;AAC9F,UAAM,MAAM,MAAM,KAAK,SAAS,KAAK,EAAE;AACvC,QAAI,CAAC,KAAK;AACR,aAAO;AAAA,IACT;AACA,WAAO;AAAA,MACL,UAAU;AAAA,QACR;AAAA,QACA,GAAI,IAAI;AAAA,MACV;AAAA,MACA,UAAU,IAAI;AAAA,IAChB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,MAAM,KAAK,IAAY,UAAqB,UAAoD;AAC9F,UAAM,OAAM,oBAAI,KAAK,GAAE,YAAY;AACnC,UAAM,WAAW,MAAM,KAAK,SAAS,KAAK,EAAE;AAC5C,UAAM,oBAAoB,UAAU;AAEpC,UAAM,SAAsC;AAAA,MAC1C,GAAG;AAAA,MACH,YAAY,mBAAmB,cAAc;AAAA,MAC7C,YAAY;AAAA,MACZ,eAAe,SAAS;AAAA,MACxB,GAAG;AAAA,IACL;AAEA,UAAM,KAAK,SAAS,OAAO;AAAA,MACzB;AAAA,MACA,UAAU;AAAA,MACV;AAAA,MACA,YAAY;AAAA,IACd,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,MAAM,OAAO,IAA8B;AACzC,WAAO,KAAK,SAAS,OAAO,EAAE;AAAA,EAChC;AACF;","names":["EventEmitter","fs","path"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -361,4 +361,157 @@ declare class InMemorySessionStore implements SessionStore {
|
|
|
361
361
|
delete(id: string): Promise<boolean>;
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
-
|
|
364
|
+
/**
|
|
365
|
+
* The shape of a row in the sessions table.
|
|
366
|
+
*
|
|
367
|
+
* Use this as a reference when defining your drizzle table schema. The `metadata`
|
|
368
|
+
* and `messages` columns should be JSON/JSONB columns (or text with JSON mode in SQLite).
|
|
369
|
+
*
|
|
370
|
+
* Example drizzle schema (PostgreSQL):
|
|
371
|
+
* ```ts
|
|
372
|
+
* import { pgTable, text, jsonb } from "drizzle-orm/pg-core";
|
|
373
|
+
*
|
|
374
|
+
* export const sessionsTable = pgTable("sessions", {
|
|
375
|
+
* id: text("id").primaryKey(),
|
|
376
|
+
* metadata: jsonb("metadata").notNull(),
|
|
377
|
+
* messages: jsonb("messages").notNull(),
|
|
378
|
+
* updated_at: text("updated_at").notNull(),
|
|
379
|
+
* });
|
|
380
|
+
* ```
|
|
381
|
+
*
|
|
382
|
+
* Example drizzle schema (SQLite):
|
|
383
|
+
* ```ts
|
|
384
|
+
* import { sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
385
|
+
*
|
|
386
|
+
* export const sessionsTable = sqliteTable("sessions", {
|
|
387
|
+
* id: text("id").primaryKey(),
|
|
388
|
+
* metadata: text("metadata", { mode: "json" }).notNull(),
|
|
389
|
+
* messages: text("messages", { mode: "json" }).notNull(),
|
|
390
|
+
* updated_at: text("updated_at").notNull(),
|
|
391
|
+
* });
|
|
392
|
+
* ```
|
|
393
|
+
*/
|
|
394
|
+
type DrizzleSessionRow = {
|
|
395
|
+
id: string;
|
|
396
|
+
/** JSON-serializable value storing `Omit<SessionMetadata, "id">`. */
|
|
397
|
+
metadata: unknown;
|
|
398
|
+
/** JSON-serializable value storing `Message[]`. */
|
|
399
|
+
messages: unknown;
|
|
400
|
+
/** ISO 8601 timestamp — denormalized from metadata for efficient ORDER BY. */
|
|
401
|
+
updated_at: string;
|
|
402
|
+
};
|
|
403
|
+
/**
|
|
404
|
+
* Adapter interface that bridges `DrizzleSessionStore` with a drizzle database instance.
|
|
405
|
+
*
|
|
406
|
+
* Implement this interface using your own drizzle `db` and table, then pass it to
|
|
407
|
+
* `DrizzleSessionStore`. This keeps drizzle out of the session package's dependencies.
|
|
408
|
+
*
|
|
409
|
+
* @example
|
|
410
|
+
* ```ts
|
|
411
|
+
* import { eq, desc } from "drizzle-orm";
|
|
412
|
+
* import { DrizzleSessionStore, type DrizzleSessionAdapter } from "@simulacra-ai/session";
|
|
413
|
+
*
|
|
414
|
+
* const adapter: DrizzleSessionAdapter = {
|
|
415
|
+
* list: () =>
|
|
416
|
+
* db.select().from(sessionsTable).orderBy(desc(sessionsTable.updated_at)),
|
|
417
|
+
*
|
|
418
|
+
* load: async (id) => {
|
|
419
|
+
* const [row] = await db
|
|
420
|
+
* .select({ metadata: sessionsTable.metadata, messages: sessionsTable.messages })
|
|
421
|
+
* .from(sessionsTable)
|
|
422
|
+
* .where(eq(sessionsTable.id, id));
|
|
423
|
+
* return row;
|
|
424
|
+
* },
|
|
425
|
+
*
|
|
426
|
+
* upsert: (row) =>
|
|
427
|
+
* db
|
|
428
|
+
* .insert(sessionsTable)
|
|
429
|
+
* .values(row)
|
|
430
|
+
* .onConflictDoUpdate({
|
|
431
|
+
* target: sessionsTable.id,
|
|
432
|
+
* set: { metadata: row.metadata, messages: row.messages, updated_at: row.updated_at },
|
|
433
|
+
* }),
|
|
434
|
+
*
|
|
435
|
+
* delete: async (id) => {
|
|
436
|
+
* const result = await db.delete(sessionsTable).where(eq(sessionsTable.id, id));
|
|
437
|
+
* return result.rowCount > 0;
|
|
438
|
+
* },
|
|
439
|
+
* };
|
|
440
|
+
*
|
|
441
|
+
* const store = new DrizzleSessionStore(adapter);
|
|
442
|
+
* ```
|
|
443
|
+
*/
|
|
444
|
+
type DrizzleSessionAdapter = {
|
|
445
|
+
/**
|
|
446
|
+
* Returns all session rows, sorted by `updated_at` descending (most recent first).
|
|
447
|
+
*/
|
|
448
|
+
list(): Promise<DrizzleSessionRow[]>;
|
|
449
|
+
/**
|
|
450
|
+
* Returns the metadata and messages for a single session, or undefined if not found.
|
|
451
|
+
*
|
|
452
|
+
* Only `metadata` and `messages` are required in the return value.
|
|
453
|
+
*/
|
|
454
|
+
load(id: string): Promise<Pick<DrizzleSessionRow, "metadata" | "messages"> | undefined>;
|
|
455
|
+
/**
|
|
456
|
+
* Inserts or updates a session row. On conflict with an existing `id`, the
|
|
457
|
+
* metadata, messages, and updated_at columns should be updated.
|
|
458
|
+
*/
|
|
459
|
+
upsert(row: DrizzleSessionRow): Promise<void>;
|
|
460
|
+
/**
|
|
461
|
+
* Deletes a session by id.
|
|
462
|
+
*
|
|
463
|
+
* @returns `true` if a row was deleted, `false` if no row with that id existed.
|
|
464
|
+
*/
|
|
465
|
+
delete(id: string): Promise<boolean>;
|
|
466
|
+
};
|
|
467
|
+
/**
|
|
468
|
+
* A database-backed implementation of `SessionStore` powered by drizzle ORM.
|
|
469
|
+
*
|
|
470
|
+
* Rather than importing drizzle directly, this store accepts a {@link DrizzleSessionAdapter}
|
|
471
|
+
* that you implement using your own drizzle instance and table. This keeps drizzle out of
|
|
472
|
+
* this package's dependencies while still providing full session persistence.
|
|
473
|
+
*
|
|
474
|
+
* See {@link DrizzleSessionAdapter} for an implementation example and
|
|
475
|
+
* {@link DrizzleSessionRow} for the expected table schema.
|
|
476
|
+
*/
|
|
477
|
+
declare class DrizzleSessionStore implements SessionStore {
|
|
478
|
+
#private;
|
|
479
|
+
constructor(adapter: DrizzleSessionAdapter);
|
|
480
|
+
/**
|
|
481
|
+
* Lists all sessions, sorted by most recently updated first.
|
|
482
|
+
*
|
|
483
|
+
* @returns A promise that resolves to an array of session metadata.
|
|
484
|
+
*/
|
|
485
|
+
list(): Promise<SessionMetadata[]>;
|
|
486
|
+
/**
|
|
487
|
+
* Loads a session by its ID.
|
|
488
|
+
*
|
|
489
|
+
* @param id - The unique identifier of the session to load.
|
|
490
|
+
* @returns A promise that resolves to the session metadata and messages, or undefined if not found.
|
|
491
|
+
*/
|
|
492
|
+
load(id: string): Promise<{
|
|
493
|
+
metadata: SessionMetadata;
|
|
494
|
+
messages: Message[];
|
|
495
|
+
} | undefined>;
|
|
496
|
+
/**
|
|
497
|
+
* Saves a session with the given messages and metadata.
|
|
498
|
+
*
|
|
499
|
+
* Creates a new session if the ID does not exist, or updates the existing session.
|
|
500
|
+
* Automatically updates the `updated_at` timestamp and `message_count`.
|
|
501
|
+
*
|
|
502
|
+
* @param id - The unique identifier of the session.
|
|
503
|
+
* @param messages - The messages to store for this session.
|
|
504
|
+
* @param metadata - Optional partial metadata to merge with existing metadata.
|
|
505
|
+
* @returns A promise that resolves when the save operation is complete.
|
|
506
|
+
*/
|
|
507
|
+
save(id: string, messages: Message[], metadata?: Partial<SessionMetadata>): Promise<void>;
|
|
508
|
+
/**
|
|
509
|
+
* Deletes a session by its ID.
|
|
510
|
+
*
|
|
511
|
+
* @param id - The unique identifier of the session to delete.
|
|
512
|
+
* @returns A promise that resolves to true if the session was deleted, false if it was not found.
|
|
513
|
+
*/
|
|
514
|
+
delete(id: string): Promise<boolean>;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export { type DrizzleSessionAdapter, type DrizzleSessionRow, DrizzleSessionStore, FileSessionStore, InMemorySessionStore, SessionManager, type SessionManagerEvents, type SessionManagerOptions, type SessionMetadata, type SessionStore };
|
package/dist/index.d.ts
CHANGED
|
@@ -361,4 +361,157 @@ declare class InMemorySessionStore implements SessionStore {
|
|
|
361
361
|
delete(id: string): Promise<boolean>;
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
-
|
|
364
|
+
/**
|
|
365
|
+
* The shape of a row in the sessions table.
|
|
366
|
+
*
|
|
367
|
+
* Use this as a reference when defining your drizzle table schema. The `metadata`
|
|
368
|
+
* and `messages` columns should be JSON/JSONB columns (or text with JSON mode in SQLite).
|
|
369
|
+
*
|
|
370
|
+
* Example drizzle schema (PostgreSQL):
|
|
371
|
+
* ```ts
|
|
372
|
+
* import { pgTable, text, jsonb } from "drizzle-orm/pg-core";
|
|
373
|
+
*
|
|
374
|
+
* export const sessionsTable = pgTable("sessions", {
|
|
375
|
+
* id: text("id").primaryKey(),
|
|
376
|
+
* metadata: jsonb("metadata").notNull(),
|
|
377
|
+
* messages: jsonb("messages").notNull(),
|
|
378
|
+
* updated_at: text("updated_at").notNull(),
|
|
379
|
+
* });
|
|
380
|
+
* ```
|
|
381
|
+
*
|
|
382
|
+
* Example drizzle schema (SQLite):
|
|
383
|
+
* ```ts
|
|
384
|
+
* import { sqliteTable, text } from "drizzle-orm/sqlite-core";
|
|
385
|
+
*
|
|
386
|
+
* export const sessionsTable = sqliteTable("sessions", {
|
|
387
|
+
* id: text("id").primaryKey(),
|
|
388
|
+
* metadata: text("metadata", { mode: "json" }).notNull(),
|
|
389
|
+
* messages: text("messages", { mode: "json" }).notNull(),
|
|
390
|
+
* updated_at: text("updated_at").notNull(),
|
|
391
|
+
* });
|
|
392
|
+
* ```
|
|
393
|
+
*/
|
|
394
|
+
type DrizzleSessionRow = {
|
|
395
|
+
id: string;
|
|
396
|
+
/** JSON-serializable value storing `Omit<SessionMetadata, "id">`. */
|
|
397
|
+
metadata: unknown;
|
|
398
|
+
/** JSON-serializable value storing `Message[]`. */
|
|
399
|
+
messages: unknown;
|
|
400
|
+
/** ISO 8601 timestamp — denormalized from metadata for efficient ORDER BY. */
|
|
401
|
+
updated_at: string;
|
|
402
|
+
};
|
|
403
|
+
/**
|
|
404
|
+
* Adapter interface that bridges `DrizzleSessionStore` with a drizzle database instance.
|
|
405
|
+
*
|
|
406
|
+
* Implement this interface using your own drizzle `db` and table, then pass it to
|
|
407
|
+
* `DrizzleSessionStore`. This keeps drizzle out of the session package's dependencies.
|
|
408
|
+
*
|
|
409
|
+
* @example
|
|
410
|
+
* ```ts
|
|
411
|
+
* import { eq, desc } from "drizzle-orm";
|
|
412
|
+
* import { DrizzleSessionStore, type DrizzleSessionAdapter } from "@simulacra-ai/session";
|
|
413
|
+
*
|
|
414
|
+
* const adapter: DrizzleSessionAdapter = {
|
|
415
|
+
* list: () =>
|
|
416
|
+
* db.select().from(sessionsTable).orderBy(desc(sessionsTable.updated_at)),
|
|
417
|
+
*
|
|
418
|
+
* load: async (id) => {
|
|
419
|
+
* const [row] = await db
|
|
420
|
+
* .select({ metadata: sessionsTable.metadata, messages: sessionsTable.messages })
|
|
421
|
+
* .from(sessionsTable)
|
|
422
|
+
* .where(eq(sessionsTable.id, id));
|
|
423
|
+
* return row;
|
|
424
|
+
* },
|
|
425
|
+
*
|
|
426
|
+
* upsert: (row) =>
|
|
427
|
+
* db
|
|
428
|
+
* .insert(sessionsTable)
|
|
429
|
+
* .values(row)
|
|
430
|
+
* .onConflictDoUpdate({
|
|
431
|
+
* target: sessionsTable.id,
|
|
432
|
+
* set: { metadata: row.metadata, messages: row.messages, updated_at: row.updated_at },
|
|
433
|
+
* }),
|
|
434
|
+
*
|
|
435
|
+
* delete: async (id) => {
|
|
436
|
+
* const result = await db.delete(sessionsTable).where(eq(sessionsTable.id, id));
|
|
437
|
+
* return result.rowCount > 0;
|
|
438
|
+
* },
|
|
439
|
+
* };
|
|
440
|
+
*
|
|
441
|
+
* const store = new DrizzleSessionStore(adapter);
|
|
442
|
+
* ```
|
|
443
|
+
*/
|
|
444
|
+
type DrizzleSessionAdapter = {
|
|
445
|
+
/**
|
|
446
|
+
* Returns all session rows, sorted by `updated_at` descending (most recent first).
|
|
447
|
+
*/
|
|
448
|
+
list(): Promise<DrizzleSessionRow[]>;
|
|
449
|
+
/**
|
|
450
|
+
* Returns the metadata and messages for a single session, or undefined if not found.
|
|
451
|
+
*
|
|
452
|
+
* Only `metadata` and `messages` are required in the return value.
|
|
453
|
+
*/
|
|
454
|
+
load(id: string): Promise<Pick<DrizzleSessionRow, "metadata" | "messages"> | undefined>;
|
|
455
|
+
/**
|
|
456
|
+
* Inserts or updates a session row. On conflict with an existing `id`, the
|
|
457
|
+
* metadata, messages, and updated_at columns should be updated.
|
|
458
|
+
*/
|
|
459
|
+
upsert(row: DrizzleSessionRow): Promise<void>;
|
|
460
|
+
/**
|
|
461
|
+
* Deletes a session by id.
|
|
462
|
+
*
|
|
463
|
+
* @returns `true` if a row was deleted, `false` if no row with that id existed.
|
|
464
|
+
*/
|
|
465
|
+
delete(id: string): Promise<boolean>;
|
|
466
|
+
};
|
|
467
|
+
/**
|
|
468
|
+
* A database-backed implementation of `SessionStore` powered by drizzle ORM.
|
|
469
|
+
*
|
|
470
|
+
* Rather than importing drizzle directly, this store accepts a {@link DrizzleSessionAdapter}
|
|
471
|
+
* that you implement using your own drizzle instance and table. This keeps drizzle out of
|
|
472
|
+
* this package's dependencies while still providing full session persistence.
|
|
473
|
+
*
|
|
474
|
+
* See {@link DrizzleSessionAdapter} for an implementation example and
|
|
475
|
+
* {@link DrizzleSessionRow} for the expected table schema.
|
|
476
|
+
*/
|
|
477
|
+
declare class DrizzleSessionStore implements SessionStore {
|
|
478
|
+
#private;
|
|
479
|
+
constructor(adapter: DrizzleSessionAdapter);
|
|
480
|
+
/**
|
|
481
|
+
* Lists all sessions, sorted by most recently updated first.
|
|
482
|
+
*
|
|
483
|
+
* @returns A promise that resolves to an array of session metadata.
|
|
484
|
+
*/
|
|
485
|
+
list(): Promise<SessionMetadata[]>;
|
|
486
|
+
/**
|
|
487
|
+
* Loads a session by its ID.
|
|
488
|
+
*
|
|
489
|
+
* @param id - The unique identifier of the session to load.
|
|
490
|
+
* @returns A promise that resolves to the session metadata and messages, or undefined if not found.
|
|
491
|
+
*/
|
|
492
|
+
load(id: string): Promise<{
|
|
493
|
+
metadata: SessionMetadata;
|
|
494
|
+
messages: Message[];
|
|
495
|
+
} | undefined>;
|
|
496
|
+
/**
|
|
497
|
+
* Saves a session with the given messages and metadata.
|
|
498
|
+
*
|
|
499
|
+
* Creates a new session if the ID does not exist, or updates the existing session.
|
|
500
|
+
* Automatically updates the `updated_at` timestamp and `message_count`.
|
|
501
|
+
*
|
|
502
|
+
* @param id - The unique identifier of the session.
|
|
503
|
+
* @param messages - The messages to store for this session.
|
|
504
|
+
* @param metadata - Optional partial metadata to merge with existing metadata.
|
|
505
|
+
* @returns A promise that resolves when the save operation is complete.
|
|
506
|
+
*/
|
|
507
|
+
save(id: string, messages: Message[], metadata?: Partial<SessionMetadata>): Promise<void>;
|
|
508
|
+
/**
|
|
509
|
+
* Deletes a session by its ID.
|
|
510
|
+
*
|
|
511
|
+
* @param id - The unique identifier of the session to delete.
|
|
512
|
+
* @returns A promise that resolves to true if the session was deleted, false if it was not found.
|
|
513
|
+
*/
|
|
514
|
+
delete(id: string): Promise<boolean>;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export { type DrizzleSessionAdapter, type DrizzleSessionRow, DrizzleSessionStore, FileSessionStore, InMemorySessionStore, SessionManager, type SessionManagerEvents, type SessionManagerOptions, type SessionMetadata, type SessionStore };
|
package/dist/index.js
CHANGED
|
@@ -567,7 +567,85 @@ var InMemorySessionStore = class {
|
|
|
567
567
|
return this.#sessions.delete(id);
|
|
568
568
|
}
|
|
569
569
|
};
|
|
570
|
+
|
|
571
|
+
// src/drizzle-session-store.ts
|
|
572
|
+
var DrizzleSessionStore = class {
|
|
573
|
+
#adapter;
|
|
574
|
+
constructor(adapter) {
|
|
575
|
+
this.#adapter = adapter;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Lists all sessions, sorted by most recently updated first.
|
|
579
|
+
*
|
|
580
|
+
* @returns A promise that resolves to an array of session metadata.
|
|
581
|
+
*/
|
|
582
|
+
async list() {
|
|
583
|
+
const rows = await this.#adapter.list();
|
|
584
|
+
return rows.map((row) => ({
|
|
585
|
+
id: row.id,
|
|
586
|
+
...row.metadata
|
|
587
|
+
}));
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Loads a session by its ID.
|
|
591
|
+
*
|
|
592
|
+
* @param id - The unique identifier of the session to load.
|
|
593
|
+
* @returns A promise that resolves to the session metadata and messages, or undefined if not found.
|
|
594
|
+
*/
|
|
595
|
+
async load(id) {
|
|
596
|
+
const row = await this.#adapter.load(id);
|
|
597
|
+
if (!row) {
|
|
598
|
+
return void 0;
|
|
599
|
+
}
|
|
600
|
+
return {
|
|
601
|
+
metadata: {
|
|
602
|
+
id,
|
|
603
|
+
...row.metadata
|
|
604
|
+
},
|
|
605
|
+
messages: row.messages
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Saves a session with the given messages and metadata.
|
|
610
|
+
*
|
|
611
|
+
* Creates a new session if the ID does not exist, or updates the existing session.
|
|
612
|
+
* Automatically updates the `updated_at` timestamp and `message_count`.
|
|
613
|
+
*
|
|
614
|
+
* @param id - The unique identifier of the session.
|
|
615
|
+
* @param messages - The messages to store for this session.
|
|
616
|
+
* @param metadata - Optional partial metadata to merge with existing metadata.
|
|
617
|
+
* @returns A promise that resolves when the save operation is complete.
|
|
618
|
+
*/
|
|
619
|
+
async save(id, messages, metadata) {
|
|
620
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
621
|
+
const existing = await this.#adapter.load(id);
|
|
622
|
+
const existing_metadata = existing?.metadata;
|
|
623
|
+
const merged = {
|
|
624
|
+
...existing_metadata,
|
|
625
|
+
created_at: existing_metadata?.created_at ?? now,
|
|
626
|
+
updated_at: now,
|
|
627
|
+
message_count: messages.length,
|
|
628
|
+
...metadata
|
|
629
|
+
};
|
|
630
|
+
await this.#adapter.upsert({
|
|
631
|
+
id,
|
|
632
|
+
metadata: merged,
|
|
633
|
+
messages,
|
|
634
|
+
updated_at: now
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Deletes a session by its ID.
|
|
639
|
+
*
|
|
640
|
+
* @param id - The unique identifier of the session to delete.
|
|
641
|
+
* @returns A promise that resolves to true if the session was deleted, false if it was not found.
|
|
642
|
+
*/
|
|
643
|
+
async delete(id) {
|
|
644
|
+
return this.#adapter.delete(id);
|
|
645
|
+
}
|
|
646
|
+
};
|
|
570
647
|
export {
|
|
648
|
+
DrizzleSessionStore,
|
|
571
649
|
FileSessionStore,
|
|
572
650
|
InMemorySessionStore,
|
|
573
651
|
SessionManager
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/session-manager.ts","../src/file-session-store.ts","../src/in-memory-session-store.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport EventEmitter from \"node:events\";\nimport type { Conversation, Message } from \"@simulacra-ai/core\";\nimport type {\n SessionMetadata,\n SessionStore,\n SessionManagerOptions,\n SessionManagerEvents,\n} from \"./types.ts\";\n\n/**\n * Manages conversation sessions including creation, loading, saving, and forking.\n *\n * The SessionManager acts as a bridge between a Conversation instance and a SessionStore,\n * handling session lifecycle, automatic saving, and session tree management through forking.\n * It supports disposable resource management and event emission for session operations.\n */\nexport class SessionManager {\n readonly #store: SessionStore;\n readonly #conversation: Conversation;\n readonly #auto_save: boolean;\n readonly #auto_slug: boolean;\n readonly #event_emitter = new EventEmitter<SessionManagerEvents>();\n readonly #child_sessions: SessionManager[] = [];\n\n #session_id?: string;\n #fork_offset = 0;\n #has_label = false;\n #disposed = false;\n\n /**\n * Creates a new SessionManager instance.\n *\n * @param store - The storage backend to use for persisting sessions.\n * @param conversation - The conversation instance to manage.\n * @param options - Optional configuration for session management behavior.\n */\n constructor(store: SessionStore, conversation: Conversation, options?: SessionManagerOptions) {\n this.#store = store;\n this.#conversation = conversation;\n this.#auto_save = options?.auto_save ?? true;\n this.#auto_slug = options?.auto_slug ?? true;\n\n if (this.#auto_save) {\n this.#conversation.on(\"message_complete\", this.#on_message_complete);\n }\n this.#conversation.on(\"create_child\", this.#on_create_child);\n this.#conversation.once(\"dispose\", this.#on_conversation_dispose);\n }\n\n /**\n * The ID of the currently active session.\n *\n * @returns The session ID if a session is active, otherwise undefined.\n */\n get current_session_id() {\n return this.#session_id;\n }\n\n /**\n * Whether a session is currently loaded.\n *\n * @returns True if a session is active, false otherwise.\n */\n get is_loaded() {\n return !!this.#session_id;\n }\n\n /**\n * Disposes of the SessionManager and cleans up resources.\n *\n * This method removes event listeners, disposes child sessions, and emits the dispose event.\n * It is called automatically when the associated conversation is disposed or when using\n * explicit resource management.\n */\n [Symbol.dispose]() {\n if (this.#disposed) {\n return;\n }\n this.#disposed = true;\n\n for (const child of this.#child_sessions) {\n child[Symbol.dispose]();\n }\n this.#child_sessions.length = 0;\n\n this.#conversation.off(\"message_complete\", this.#on_message_complete);\n this.#conversation.off(\"create_child\", this.#on_create_child);\n this.#conversation.off(\"dispose\", this.#on_conversation_dispose);\n this.#event_emitter.emit(\"dispose\");\n this.#event_emitter.removeAllListeners();\n }\n\n /**\n * Starts a new session with a freshly generated ID.\n *\n * This method clears the conversation history and creates a new session. If auto_save\n * is enabled or a label is provided, the session is immediately saved to the store.\n *\n * @param label - Optional label to assign to the new session.\n * @returns The generated session ID.\n */\n start_new(label?: string): string {\n this.#session_id = randomUUID();\n this.#fork_offset = 0;\n this.#has_label = !!label;\n if (this.#conversation.messages.length > 0) {\n this.#conversation.clear();\n }\n if (label || this.#auto_save) {\n this.save({ label }).catch((error) => {\n this.#event_emitter.emit(\"lifecycle_error\", {\n error,\n operation: \"save\",\n context: { session_id: this.#session_id },\n });\n });\n }\n return this.#session_id;\n }\n\n /**\n * Creates a new session as a fork of an existing parent session.\n *\n * A fork creates a new session that inherits messages from the parent session up to\n * the fork point. If detached, the fork does not inherit any messages from the parent\n * but still maintains a parent reference for organizational purposes.\n *\n * @param parent_session_id - The ID of the session to fork from.\n * @param options - Optional configuration for the fork operation.\n * @param options.detached - Whether to create a detached fork that does not inherit parent messages.\n * @returns A promise that resolves to the generated session ID for the fork.\n */\n async fork(parent_session_id: string, options?: { detached?: boolean }): Promise<string> {\n const detached = options?.detached ?? false;\n this.#session_id = randomUUID();\n this.#has_label = false;\n if (!detached) {\n this.#conversation.clear();\n }\n\n let fork_message_id: string | undefined;\n\n if (!detached) {\n const messages = await this.#resolve_messages(parent_session_id);\n if (messages.length > 0) {\n this.#conversation.load(messages);\n this.#fork_offset = messages.length;\n fork_message_id = messages.at(-1)?.id;\n } else {\n this.#fork_offset = 0;\n }\n } else {\n this.#fork_offset = 0;\n }\n\n const parent_result = await this.#store.load(parent_session_id);\n if (parent_result?.metadata.checkpoint_state) {\n this.#conversation.checkpoint_state = parent_result.metadata.checkpoint_state;\n }\n\n await this.save({\n parent_id: parent_session_id,\n fork_message_id,\n detached,\n });\n\n return this.#session_id;\n }\n\n /**\n * Loads a session by ID or loads the most recent session if no ID is provided.\n *\n * If no session ID is provided and no sessions exist in the store, this method\n * starts a new session automatically. Loading a session resolves its full message\n * history by recursively following parent references.\n *\n * @param id - The ID of the session to load, or undefined to load the most recent session.\n * @returns A promise that resolves when the session is loaded.\n * @throws {Error} If the specified session ID is not found in the store.\n */\n async load(id?: string): Promise<void> {\n if (id) {\n const messages = await this.#resolve_messages(id);\n const result = await this.#store.load(id);\n if (!result) {\n throw new Error(`session not found: ${id}`);\n }\n this.#session_id = id;\n this.#has_label = !!result.metadata.label;\n this.#conversation.clear();\n this.#fork_offset = messages.length - result.messages.length;\n this.#conversation.load(messages);\n this.#conversation.checkpoint_state = result.metadata.checkpoint_state;\n this.#emit_load(messages);\n return;\n }\n\n const sessions = await this.#store.list();\n if (!sessions.length) {\n this.start_new();\n return;\n }\n\n const latest = sessions[0];\n return this.load(latest.id);\n }\n\n /**\n * Saves the current session to the store.\n *\n * This method persists only the messages owned by this session, excluding any messages\n * inherited from parent sessions. If auto_slug is enabled and no label has been set,\n * a label is automatically generated from the first user message.\n *\n * @param metadata - Optional partial metadata to update on the session.\n * @returns A promise that resolves when the save operation is complete.\n * @throws {Error} If no session is currently active.\n */\n async save(\n metadata?: Partial<\n Pick<\n SessionMetadata,\n \"label\" | \"provider\" | \"model\" | \"parent_id\" | \"fork_message_id\" | \"detached\"\n >\n >,\n ) {\n if (!this.#session_id) {\n throw new Error(\"no active session\");\n }\n const all_messages = this.#conversation.messages;\n const owned_messages = [...all_messages].slice(this.#fork_offset);\n\n if (this.#auto_slug && !metadata?.label && !this.#has_label) {\n const slug = SessionManager.#derive_slug(all_messages);\n if (slug) {\n metadata = { ...metadata, label: slug };\n this.#has_label = true;\n }\n }\n\n const conversation_metadata: Partial<SessionMetadata> = { ...metadata };\n if (this.#conversation.is_checkpoint) {\n conversation_metadata.is_checkpoint = true;\n }\n const checkpoint_state = this.#conversation.checkpoint_state;\n if (checkpoint_state) {\n conversation_metadata.checkpoint_state = { ...checkpoint_state };\n }\n\n await this.#store.save(this.#session_id, owned_messages, conversation_metadata);\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this.#event_emitter as any).emit(\"save\", {\n id: this.#session_id,\n messages: Object.freeze([...all_messages]),\n });\n }\n\n /**\n * Lists all sessions stored in the store.\n *\n * @returns A promise that resolves to an array of session metadata, typically sorted by last update time.\n */\n async list() {\n return this.#store.list();\n }\n\n /**\n * Deletes a session from the store.\n *\n * If the deleted session is currently active, the conversation is cleared and the\n * current session is unset.\n *\n * @param id - The ID of the session to delete.\n * @returns A promise that resolves to true if the session was deleted, false if it was not found.\n */\n async delete(id: string) {\n if (id === this.#session_id) {\n this.#session_id = undefined;\n this.#conversation.clear();\n }\n return this.#store.delete(id);\n }\n\n /**\n * Renames a session by updating its label.\n *\n * @param id - The ID of the session to rename.\n * @param label - The new label to assign to the session.\n * @returns A promise that resolves when the rename operation is complete.\n * @throws {Error} If the specified session ID is not found in the store.\n */\n async rename(id: string, label: string) {\n const result = await this.#store.load(id);\n if (!result) {\n throw new Error(`session not found: ${id}`);\n }\n await this.#store.save(id, result.messages, { label });\n if (id === this.#session_id) {\n this.#has_label = true;\n }\n }\n\n /**\n * Registers an event listener for the specified event.\n *\n * @param event - The name of the event to listen for.\n * @param listener - The callback function to invoke when the event is emitted.\n * @returns This SessionManager instance for method chaining.\n */\n on<E extends keyof SessionManagerEvents>(\n event: E,\n listener: (...args: SessionManagerEvents[E]) => void,\n ): this {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this.#event_emitter as any).on(event, listener);\n return this;\n }\n\n /**\n * Removes an event listener for the specified event.\n *\n * @param event - The name of the event to stop listening for.\n * @param listener - The callback function to remove.\n * @returns This SessionManager instance for method chaining.\n */\n off<E extends keyof SessionManagerEvents>(\n event: E,\n listener: (...args: SessionManagerEvents[E]) => void,\n ): this {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this.#event_emitter as any).off(event, listener);\n return this;\n }\n\n #emit_load(messages: Readonly<Message[]>) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this.#event_emitter as any).emit(\"load\", {\n id: this.#session_id,\n messages: Object.freeze([...messages]),\n });\n }\n\n async #resolve_messages(id: string): Promise<Message[]> {\n const result = await this.#store.load(id);\n if (!result) {\n return [];\n }\n\n const { metadata, messages } = result;\n\n if (metadata.parent_id && !metadata.detached) {\n const parent_messages = await this.#resolve_messages(metadata.parent_id);\n if (metadata.fork_message_id) {\n const cut = parent_messages.findIndex((m) => m.id === metadata.fork_message_id);\n if (cut >= 0) {\n return [...parent_messages.slice(0, cut + 1), ...messages];\n }\n }\n return [...parent_messages, ...messages];\n }\n\n return messages;\n }\n\n static #derive_slug(messages: readonly Readonly<Message>[]): string | undefined {\n const first_user = messages.find((m) => m.role === \"user\");\n if (!first_user) {\n return undefined;\n }\n\n const text_content = first_user.content.find((c) => c.type === \"text\");\n if (!text_content || text_content.type !== \"text\" || !text_content.text) {\n return undefined;\n }\n\n const raw = text_content.text.trim();\n if (!raw) {\n return undefined;\n }\n\n const truncated = raw.slice(0, 60);\n const at_boundary = truncated.replace(/\\s+\\S*$/, \"\");\n return (at_boundary || truncated).slice(0, 50);\n }\n\n #on_create_child = (child: Conversation) => {\n if (!this.#session_id) {\n return;\n }\n\n const child_session = new SessionManager(this.#store, child, {\n auto_save: this.#auto_save,\n auto_slug: !child.is_checkpoint && this.#auto_slug,\n });\n child_session.fork(this.#session_id, { detached: true }).catch((error) => {\n this.#event_emitter.emit(\"lifecycle_error\", { error, operation: \"fork\" });\n });\n this.#child_sessions.push(child_session);\n\n child.once(\"dispose\", () => {\n child_session[Symbol.dispose]();\n const idx = this.#child_sessions.indexOf(child_session);\n if (idx >= 0) {\n this.#child_sessions.splice(idx, 1);\n }\n });\n };\n\n #on_message_complete = () => {\n if (this.#session_id) {\n this.save().catch((error) => {\n this.#event_emitter.emit(\"lifecycle_error\", {\n error,\n operation: \"save\",\n context: { session_id: this.#session_id },\n });\n });\n }\n };\n\n #on_conversation_dispose = () => this[Symbol.dispose]();\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { Message } from \"@simulacra-ai/core\";\nimport type { SessionMetadata, SessionStore } from \"./types.ts\";\n\ninterface SessionFile {\n metadata: Omit<SessionMetadata, \"id\">;\n messages: Message[];\n}\n\n/**\n * A file-based implementation of SessionStore.\n *\n * This store persists sessions as JSON files in a specified directory. Each session\n * is stored in a separate file named with its session ID. The store also maintains\n * hard links for fork relationships to enable efficient querying of session trees.\n */\nexport class FileSessionStore implements SessionStore {\n readonly #root: string;\n\n /**\n * Creates a new FileSessionStore instance.\n *\n * @param root - The absolute path to the directory where session files will be stored.\n */\n constructor(root: string) {\n this.#root = root;\n }\n\n /**\n * Lists all sessions stored in the file system.\n *\n * Reads all JSON files from the root directory and parses their metadata.\n *\n * @returns A promise that resolves to an array of session metadata, sorted by most recently updated first.\n */\n async list(): Promise<SessionMetadata[]> {\n await this.#ensure_dir();\n const sessions: SessionMetadata[] = [];\n\n const entries = await fs.readdir(this.#root, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isFile() || !entry.name.endsWith(\".json\")) {\n continue;\n }\n const session = await this.#read_session(\n path.join(this.#root, entry.name),\n entry.name.replace(/\\.json$/, \"\"),\n );\n if (session) {\n sessions.push(session);\n }\n }\n\n return sessions.sort((a, b) => b.updated_at.localeCompare(a.updated_at));\n }\n\n /**\n * Loads a session from the file system.\n *\n * @param id - The unique identifier of the session to load.\n * @returns A promise that resolves to the session metadata and messages, or undefined if not found.\n */\n async load(id: string) {\n return this.#read_file(path.join(this.#root, `${id}.json`), id);\n }\n\n /**\n * Saves a session to the file system.\n *\n * Creates a new session file if the ID does not exist, or updates an existing file.\n * Automatically updates the updated_at timestamp and message_count. If the session\n * has a parent, creates a hard link in the parent's fork directory for efficient querying.\n *\n * @param id - The unique identifier of the session.\n * @param messages - The messages to store for this session.\n * @param metadata - Optional partial metadata to merge with existing metadata.\n * @returns A promise that resolves when the save operation is complete.\n */\n async save(id: string, messages: Message[], metadata?: Partial<SessionMetadata>) {\n const now = new Date().toISOString();\n await this.#ensure_dir();\n\n const file_path = path.join(this.#root, `${id}.json`);\n const existing = await this.#read_file(file_path, id);\n const existing_metadata: Partial<SessionMetadata> = existing?.metadata ?? {};\n const parent_id = metadata?.parent_id ?? existing_metadata.parent_id;\n\n const file: SessionFile = {\n metadata: {\n ...existing_metadata,\n created_at: existing_metadata.created_at ?? now,\n updated_at: now,\n message_count: messages.length,\n ...metadata,\n },\n messages,\n };\n\n await fs.writeFile(file_path, JSON.stringify(file, null, 2), \"utf8\");\n\n if (parent_id) {\n await this.#ensure_fork_link(parent_id, id);\n }\n }\n\n /**\n * Deletes a session from the file system.\n *\n * Removes the session file, any hard links in parent fork directories, and the session's\n * own fork directory if it exists.\n *\n * @param id - The unique identifier of the session to delete.\n * @returns A promise that resolves to true if the session was deleted, false if it was not found.\n */\n async delete(id: string) {\n const file_path = path.join(this.#root, `${id}.json`);\n try {\n const result = await this.#read_file(file_path, id);\n await fs.unlink(file_path);\n\n if (result?.metadata.parent_id) {\n const link = path.join(this.#root, `${result.metadata.parent_id}-forks`, `${id}.json`);\n try {\n await fs.unlink(link);\n } catch {\n /* link may not exist */\n }\n }\n\n const fork_dir = path.join(this.#root, `${id}-forks`);\n try {\n await fs.rm(fork_dir, { recursive: true });\n } catch {\n /* no forks */\n }\n\n return true;\n } catch {\n return false;\n }\n }\n\n async #ensure_fork_link(parent_id: string, fork_id: string) {\n const fork_dir = path.join(this.#root, `${parent_id}-forks`);\n await fs.mkdir(fork_dir, { recursive: true });\n\n const canonical = path.join(this.#root, `${fork_id}.json`);\n const link = path.join(fork_dir, `${fork_id}.json`);\n\n try {\n await fs.unlink(link);\n } catch {\n /* doesn't exist yet */\n }\n\n try {\n await fs.link(canonical, link);\n } catch {\n // hard links can fail across filesystems — fall back to no index\n }\n }\n\n async #read_file(file_path: string, id: string) {\n try {\n const content = await fs.readFile(file_path, \"utf8\");\n const data: SessionFile = JSON.parse(content);\n return {\n metadata: { id, ...data.metadata } as SessionMetadata,\n messages: data.messages,\n };\n } catch {\n return undefined;\n }\n }\n\n async #read_session(file_path: string, id: string): Promise<SessionMetadata | undefined> {\n try {\n const content = await fs.readFile(file_path, \"utf8\");\n const data: SessionFile = JSON.parse(content);\n return { id, ...data.metadata };\n } catch {\n return undefined;\n }\n }\n\n async #ensure_dir() {\n await fs.mkdir(this.#root, { recursive: true });\n }\n}\n","import type { Message } from \"@simulacra-ai/core\";\nimport type { SessionMetadata, SessionStore } from \"./types.ts\";\n\n/**\n * An in-memory implementation of SessionStore.\n *\n * This store keeps all session data in memory using a Map. Data is not persisted\n * across process restarts. Useful for testing or scenarios where persistence is not required.\n */\nexport class InMemorySessionStore implements SessionStore {\n readonly #sessions = new Map<string, { metadata: SessionMetadata; messages: Message[] }>();\n\n /**\n * Lists all sessions stored in memory.\n *\n * @returns A promise that resolves to an array of session metadata, sorted by most recently updated first.\n */\n async list(): Promise<SessionMetadata[]> {\n return [...this.#sessions.values()]\n .map((s) => s.metadata)\n .sort((a, b) => b.updated_at.localeCompare(a.updated_at));\n }\n\n /**\n * Loads a session from memory.\n *\n * Returns a deep clone of the stored data to prevent external mutations.\n *\n * @param id - The unique identifier of the session to load.\n * @returns A promise that resolves to the session metadata and messages, or undefined if not found.\n */\n async load(id: string) {\n const entry = this.#sessions.get(id);\n if (!entry) {\n return undefined;\n }\n return {\n metadata: { ...entry.metadata },\n messages: structuredClone(entry.messages),\n };\n }\n\n /**\n * Saves a session to memory.\n *\n * Creates a new session if the ID does not exist, or updates an existing session.\n * Automatically updates the updated_at timestamp and message_count.\n *\n * @param id - The unique identifier of the session.\n * @param messages - The messages to store for this session.\n * @param metadata - Optional partial metadata to merge with existing metadata.\n * @returns A promise that resolves when the save operation is complete.\n */\n async save(id: string, messages: Message[], metadata?: Partial<SessionMetadata>) {\n const now = new Date().toISOString();\n const existing = this.#sessions.get(id);\n\n this.#sessions.set(id, {\n metadata: {\n id,\n ...existing?.metadata,\n created_at: existing?.metadata.created_at ?? now,\n updated_at: now,\n message_count: messages.length,\n ...metadata,\n },\n messages: structuredClone(messages),\n });\n }\n\n /**\n * Deletes a session from memory.\n *\n * @param id - The unique identifier of the session to delete.\n * @returns A promise that resolves to true if the session was deleted, false if it was not found.\n */\n async delete(id: string) {\n return this.#sessions.delete(id);\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/session-manager.ts","../src/file-session-store.ts","../src/in-memory-session-store.ts","../src/drizzle-session-store.ts"],"sourcesContent":["import { randomUUID } from \"node:crypto\";\nimport EventEmitter from \"node:events\";\nimport type { Conversation, Message } from \"@simulacra-ai/core\";\nimport type {\n SessionMetadata,\n SessionStore,\n SessionManagerOptions,\n SessionManagerEvents,\n} from \"./types.ts\";\n\n/**\n * Manages conversation sessions including creation, loading, saving, and forking.\n *\n * The SessionManager acts as a bridge between a Conversation instance and a SessionStore,\n * handling session lifecycle, automatic saving, and session tree management through forking.\n * It supports disposable resource management and event emission for session operations.\n */\nexport class SessionManager {\n readonly #store: SessionStore;\n readonly #conversation: Conversation;\n readonly #auto_save: boolean;\n readonly #auto_slug: boolean;\n readonly #event_emitter = new EventEmitter<SessionManagerEvents>();\n readonly #child_sessions: SessionManager[] = [];\n\n #session_id?: string;\n #fork_offset = 0;\n #has_label = false;\n #disposed = false;\n\n /**\n * Creates a new SessionManager instance.\n *\n * @param store - The storage backend to use for persisting sessions.\n * @param conversation - The conversation instance to manage.\n * @param options - Optional configuration for session management behavior.\n */\n constructor(store: SessionStore, conversation: Conversation, options?: SessionManagerOptions) {\n this.#store = store;\n this.#conversation = conversation;\n this.#auto_save = options?.auto_save ?? true;\n this.#auto_slug = options?.auto_slug ?? true;\n\n if (this.#auto_save) {\n this.#conversation.on(\"message_complete\", this.#on_message_complete);\n }\n this.#conversation.on(\"create_child\", this.#on_create_child);\n this.#conversation.once(\"dispose\", this.#on_conversation_dispose);\n }\n\n /**\n * The ID of the currently active session.\n *\n * @returns The session ID if a session is active, otherwise undefined.\n */\n get current_session_id() {\n return this.#session_id;\n }\n\n /**\n * Whether a session is currently loaded.\n *\n * @returns True if a session is active, false otherwise.\n */\n get is_loaded() {\n return !!this.#session_id;\n }\n\n /**\n * Disposes of the SessionManager and cleans up resources.\n *\n * This method removes event listeners, disposes child sessions, and emits the dispose event.\n * It is called automatically when the associated conversation is disposed or when using\n * explicit resource management.\n */\n [Symbol.dispose]() {\n if (this.#disposed) {\n return;\n }\n this.#disposed = true;\n\n for (const child of this.#child_sessions) {\n child[Symbol.dispose]();\n }\n this.#child_sessions.length = 0;\n\n this.#conversation.off(\"message_complete\", this.#on_message_complete);\n this.#conversation.off(\"create_child\", this.#on_create_child);\n this.#conversation.off(\"dispose\", this.#on_conversation_dispose);\n this.#event_emitter.emit(\"dispose\");\n this.#event_emitter.removeAllListeners();\n }\n\n /**\n * Starts a new session with a freshly generated ID.\n *\n * This method clears the conversation history and creates a new session. If auto_save\n * is enabled or a label is provided, the session is immediately saved to the store.\n *\n * @param label - Optional label to assign to the new session.\n * @returns The generated session ID.\n */\n start_new(label?: string): string {\n this.#session_id = randomUUID();\n this.#fork_offset = 0;\n this.#has_label = !!label;\n if (this.#conversation.messages.length > 0) {\n this.#conversation.clear();\n }\n if (label || this.#auto_save) {\n this.save({ label }).catch((error) => {\n this.#event_emitter.emit(\"lifecycle_error\", {\n error,\n operation: \"save\",\n context: { session_id: this.#session_id },\n });\n });\n }\n return this.#session_id;\n }\n\n /**\n * Creates a new session as a fork of an existing parent session.\n *\n * A fork creates a new session that inherits messages from the parent session up to\n * the fork point. If detached, the fork does not inherit any messages from the parent\n * but still maintains a parent reference for organizational purposes.\n *\n * @param parent_session_id - The ID of the session to fork from.\n * @param options - Optional configuration for the fork operation.\n * @param options.detached - Whether to create a detached fork that does not inherit parent messages.\n * @returns A promise that resolves to the generated session ID for the fork.\n */\n async fork(parent_session_id: string, options?: { detached?: boolean }): Promise<string> {\n const detached = options?.detached ?? false;\n this.#session_id = randomUUID();\n this.#has_label = false;\n if (!detached) {\n this.#conversation.clear();\n }\n\n let fork_message_id: string | undefined;\n\n if (!detached) {\n const messages = await this.#resolve_messages(parent_session_id);\n if (messages.length > 0) {\n this.#conversation.load(messages);\n this.#fork_offset = messages.length;\n fork_message_id = messages.at(-1)?.id;\n } else {\n this.#fork_offset = 0;\n }\n } else {\n this.#fork_offset = 0;\n }\n\n const parent_result = await this.#store.load(parent_session_id);\n if (parent_result?.metadata.checkpoint_state) {\n this.#conversation.checkpoint_state = parent_result.metadata.checkpoint_state;\n }\n\n await this.save({\n parent_id: parent_session_id,\n fork_message_id,\n detached,\n });\n\n return this.#session_id;\n }\n\n /**\n * Loads a session by ID or loads the most recent session if no ID is provided.\n *\n * If no session ID is provided and no sessions exist in the store, this method\n * starts a new session automatically. Loading a session resolves its full message\n * history by recursively following parent references.\n *\n * @param id - The ID of the session to load, or undefined to load the most recent session.\n * @returns A promise that resolves when the session is loaded.\n * @throws {Error} If the specified session ID is not found in the store.\n */\n async load(id?: string): Promise<void> {\n if (id) {\n const messages = await this.#resolve_messages(id);\n const result = await this.#store.load(id);\n if (!result) {\n throw new Error(`session not found: ${id}`);\n }\n this.#session_id = id;\n this.#has_label = !!result.metadata.label;\n this.#conversation.clear();\n this.#fork_offset = messages.length - result.messages.length;\n this.#conversation.load(messages);\n this.#conversation.checkpoint_state = result.metadata.checkpoint_state;\n this.#emit_load(messages);\n return;\n }\n\n const sessions = await this.#store.list();\n if (!sessions.length) {\n this.start_new();\n return;\n }\n\n const latest = sessions[0];\n return this.load(latest.id);\n }\n\n /**\n * Saves the current session to the store.\n *\n * This method persists only the messages owned by this session, excluding any messages\n * inherited from parent sessions. If auto_slug is enabled and no label has been set,\n * a label is automatically generated from the first user message.\n *\n * @param metadata - Optional partial metadata to update on the session.\n * @returns A promise that resolves when the save operation is complete.\n * @throws {Error} If no session is currently active.\n */\n async save(\n metadata?: Partial<\n Pick<\n SessionMetadata,\n \"label\" | \"provider\" | \"model\" | \"parent_id\" | \"fork_message_id\" | \"detached\"\n >\n >,\n ) {\n if (!this.#session_id) {\n throw new Error(\"no active session\");\n }\n const all_messages = this.#conversation.messages;\n const owned_messages = [...all_messages].slice(this.#fork_offset);\n\n if (this.#auto_slug && !metadata?.label && !this.#has_label) {\n const slug = SessionManager.#derive_slug(all_messages);\n if (slug) {\n metadata = { ...metadata, label: slug };\n this.#has_label = true;\n }\n }\n\n const conversation_metadata: Partial<SessionMetadata> = { ...metadata };\n if (this.#conversation.is_checkpoint) {\n conversation_metadata.is_checkpoint = true;\n }\n const checkpoint_state = this.#conversation.checkpoint_state;\n if (checkpoint_state) {\n conversation_metadata.checkpoint_state = { ...checkpoint_state };\n }\n\n await this.#store.save(this.#session_id, owned_messages, conversation_metadata);\n\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this.#event_emitter as any).emit(\"save\", {\n id: this.#session_id,\n messages: Object.freeze([...all_messages]),\n });\n }\n\n /**\n * Lists all sessions stored in the store.\n *\n * @returns A promise that resolves to an array of session metadata, typically sorted by last update time.\n */\n async list() {\n return this.#store.list();\n }\n\n /**\n * Deletes a session from the store.\n *\n * If the deleted session is currently active, the conversation is cleared and the\n * current session is unset.\n *\n * @param id - The ID of the session to delete.\n * @returns A promise that resolves to true if the session was deleted, false if it was not found.\n */\n async delete(id: string) {\n if (id === this.#session_id) {\n this.#session_id = undefined;\n this.#conversation.clear();\n }\n return this.#store.delete(id);\n }\n\n /**\n * Renames a session by updating its label.\n *\n * @param id - The ID of the session to rename.\n * @param label - The new label to assign to the session.\n * @returns A promise that resolves when the rename operation is complete.\n * @throws {Error} If the specified session ID is not found in the store.\n */\n async rename(id: string, label: string) {\n const result = await this.#store.load(id);\n if (!result) {\n throw new Error(`session not found: ${id}`);\n }\n await this.#store.save(id, result.messages, { label });\n if (id === this.#session_id) {\n this.#has_label = true;\n }\n }\n\n /**\n * Registers an event listener for the specified event.\n *\n * @param event - The name of the event to listen for.\n * @param listener - The callback function to invoke when the event is emitted.\n * @returns This SessionManager instance for method chaining.\n */\n on<E extends keyof SessionManagerEvents>(\n event: E,\n listener: (...args: SessionManagerEvents[E]) => void,\n ): this {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this.#event_emitter as any).on(event, listener);\n return this;\n }\n\n /**\n * Removes an event listener for the specified event.\n *\n * @param event - The name of the event to stop listening for.\n * @param listener - The callback function to remove.\n * @returns This SessionManager instance for method chaining.\n */\n off<E extends keyof SessionManagerEvents>(\n event: E,\n listener: (...args: SessionManagerEvents[E]) => void,\n ): this {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this.#event_emitter as any).off(event, listener);\n return this;\n }\n\n #emit_load(messages: Readonly<Message[]>) {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n (this.#event_emitter as any).emit(\"load\", {\n id: this.#session_id,\n messages: Object.freeze([...messages]),\n });\n }\n\n async #resolve_messages(id: string): Promise<Message[]> {\n const result = await this.#store.load(id);\n if (!result) {\n return [];\n }\n\n const { metadata, messages } = result;\n\n if (metadata.parent_id && !metadata.detached) {\n const parent_messages = await this.#resolve_messages(metadata.parent_id);\n if (metadata.fork_message_id) {\n const cut = parent_messages.findIndex((m) => m.id === metadata.fork_message_id);\n if (cut >= 0) {\n return [...parent_messages.slice(0, cut + 1), ...messages];\n }\n }\n return [...parent_messages, ...messages];\n }\n\n return messages;\n }\n\n static #derive_slug(messages: readonly Readonly<Message>[]): string | undefined {\n const first_user = messages.find((m) => m.role === \"user\");\n if (!first_user) {\n return undefined;\n }\n\n const text_content = first_user.content.find((c) => c.type === \"text\");\n if (!text_content || text_content.type !== \"text\" || !text_content.text) {\n return undefined;\n }\n\n const raw = text_content.text.trim();\n if (!raw) {\n return undefined;\n }\n\n const truncated = raw.slice(0, 60);\n const at_boundary = truncated.replace(/\\s+\\S*$/, \"\");\n return (at_boundary || truncated).slice(0, 50);\n }\n\n #on_create_child = (child: Conversation) => {\n if (!this.#session_id) {\n return;\n }\n\n const child_session = new SessionManager(this.#store, child, {\n auto_save: this.#auto_save,\n auto_slug: !child.is_checkpoint && this.#auto_slug,\n });\n child_session.fork(this.#session_id, { detached: true }).catch((error) => {\n this.#event_emitter.emit(\"lifecycle_error\", { error, operation: \"fork\" });\n });\n this.#child_sessions.push(child_session);\n\n child.once(\"dispose\", () => {\n child_session[Symbol.dispose]();\n const idx = this.#child_sessions.indexOf(child_session);\n if (idx >= 0) {\n this.#child_sessions.splice(idx, 1);\n }\n });\n };\n\n #on_message_complete = () => {\n if (this.#session_id) {\n this.save().catch((error) => {\n this.#event_emitter.emit(\"lifecycle_error\", {\n error,\n operation: \"save\",\n context: { session_id: this.#session_id },\n });\n });\n }\n };\n\n #on_conversation_dispose = () => this[Symbol.dispose]();\n}\n","import fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { Message } from \"@simulacra-ai/core\";\nimport type { SessionMetadata, SessionStore } from \"./types.ts\";\n\ninterface SessionFile {\n metadata: Omit<SessionMetadata, \"id\">;\n messages: Message[];\n}\n\n/**\n * A file-based implementation of SessionStore.\n *\n * This store persists sessions as JSON files in a specified directory. Each session\n * is stored in a separate file named with its session ID. The store also maintains\n * hard links for fork relationships to enable efficient querying of session trees.\n */\nexport class FileSessionStore implements SessionStore {\n readonly #root: string;\n\n /**\n * Creates a new FileSessionStore instance.\n *\n * @param root - The absolute path to the directory where session files will be stored.\n */\n constructor(root: string) {\n this.#root = root;\n }\n\n /**\n * Lists all sessions stored in the file system.\n *\n * Reads all JSON files from the root directory and parses their metadata.\n *\n * @returns A promise that resolves to an array of session metadata, sorted by most recently updated first.\n */\n async list(): Promise<SessionMetadata[]> {\n await this.#ensure_dir();\n const sessions: SessionMetadata[] = [];\n\n const entries = await fs.readdir(this.#root, { withFileTypes: true });\n for (const entry of entries) {\n if (!entry.isFile() || !entry.name.endsWith(\".json\")) {\n continue;\n }\n const session = await this.#read_session(\n path.join(this.#root, entry.name),\n entry.name.replace(/\\.json$/, \"\"),\n );\n if (session) {\n sessions.push(session);\n }\n }\n\n return sessions.sort((a, b) => b.updated_at.localeCompare(a.updated_at));\n }\n\n /**\n * Loads a session from the file system.\n *\n * @param id - The unique identifier of the session to load.\n * @returns A promise that resolves to the session metadata and messages, or undefined if not found.\n */\n async load(id: string) {\n return this.#read_file(path.join(this.#root, `${id}.json`), id);\n }\n\n /**\n * Saves a session to the file system.\n *\n * Creates a new session file if the ID does not exist, or updates an existing file.\n * Automatically updates the updated_at timestamp and message_count. If the session\n * has a parent, creates a hard link in the parent's fork directory for efficient querying.\n *\n * @param id - The unique identifier of the session.\n * @param messages - The messages to store for this session.\n * @param metadata - Optional partial metadata to merge with existing metadata.\n * @returns A promise that resolves when the save operation is complete.\n */\n async save(id: string, messages: Message[], metadata?: Partial<SessionMetadata>) {\n const now = new Date().toISOString();\n await this.#ensure_dir();\n\n const file_path = path.join(this.#root, `${id}.json`);\n const existing = await this.#read_file(file_path, id);\n const existing_metadata: Partial<SessionMetadata> = existing?.metadata ?? {};\n const parent_id = metadata?.parent_id ?? existing_metadata.parent_id;\n\n const file: SessionFile = {\n metadata: {\n ...existing_metadata,\n created_at: existing_metadata.created_at ?? now,\n updated_at: now,\n message_count: messages.length,\n ...metadata,\n },\n messages,\n };\n\n await fs.writeFile(file_path, JSON.stringify(file, null, 2), \"utf8\");\n\n if (parent_id) {\n await this.#ensure_fork_link(parent_id, id);\n }\n }\n\n /**\n * Deletes a session from the file system.\n *\n * Removes the session file, any hard links in parent fork directories, and the session's\n * own fork directory if it exists.\n *\n * @param id - The unique identifier of the session to delete.\n * @returns A promise that resolves to true if the session was deleted, false if it was not found.\n */\n async delete(id: string) {\n const file_path = path.join(this.#root, `${id}.json`);\n try {\n const result = await this.#read_file(file_path, id);\n await fs.unlink(file_path);\n\n if (result?.metadata.parent_id) {\n const link = path.join(this.#root, `${result.metadata.parent_id}-forks`, `${id}.json`);\n try {\n await fs.unlink(link);\n } catch {\n /* link may not exist */\n }\n }\n\n const fork_dir = path.join(this.#root, `${id}-forks`);\n try {\n await fs.rm(fork_dir, { recursive: true });\n } catch {\n /* no forks */\n }\n\n return true;\n } catch {\n return false;\n }\n }\n\n async #ensure_fork_link(parent_id: string, fork_id: string) {\n const fork_dir = path.join(this.#root, `${parent_id}-forks`);\n await fs.mkdir(fork_dir, { recursive: true });\n\n const canonical = path.join(this.#root, `${fork_id}.json`);\n const link = path.join(fork_dir, `${fork_id}.json`);\n\n try {\n await fs.unlink(link);\n } catch {\n /* doesn't exist yet */\n }\n\n try {\n await fs.link(canonical, link);\n } catch {\n // hard links can fail across filesystems — fall back to no index\n }\n }\n\n async #read_file(file_path: string, id: string) {\n try {\n const content = await fs.readFile(file_path, \"utf8\");\n const data: SessionFile = JSON.parse(content);\n return {\n metadata: { id, ...data.metadata } as SessionMetadata,\n messages: data.messages,\n };\n } catch {\n return undefined;\n }\n }\n\n async #read_session(file_path: string, id: string): Promise<SessionMetadata | undefined> {\n try {\n const content = await fs.readFile(file_path, \"utf8\");\n const data: SessionFile = JSON.parse(content);\n return { id, ...data.metadata };\n } catch {\n return undefined;\n }\n }\n\n async #ensure_dir() {\n await fs.mkdir(this.#root, { recursive: true });\n }\n}\n","import type { Message } from \"@simulacra-ai/core\";\nimport type { SessionMetadata, SessionStore } from \"./types.ts\";\n\n/**\n * An in-memory implementation of SessionStore.\n *\n * This store keeps all session data in memory using a Map. Data is not persisted\n * across process restarts. Useful for testing or scenarios where persistence is not required.\n */\nexport class InMemorySessionStore implements SessionStore {\n readonly #sessions = new Map<string, { metadata: SessionMetadata; messages: Message[] }>();\n\n /**\n * Lists all sessions stored in memory.\n *\n * @returns A promise that resolves to an array of session metadata, sorted by most recently updated first.\n */\n async list(): Promise<SessionMetadata[]> {\n return [...this.#sessions.values()]\n .map((s) => s.metadata)\n .sort((a, b) => b.updated_at.localeCompare(a.updated_at));\n }\n\n /**\n * Loads a session from memory.\n *\n * Returns a deep clone of the stored data to prevent external mutations.\n *\n * @param id - The unique identifier of the session to load.\n * @returns A promise that resolves to the session metadata and messages, or undefined if not found.\n */\n async load(id: string) {\n const entry = this.#sessions.get(id);\n if (!entry) {\n return undefined;\n }\n return {\n metadata: { ...entry.metadata },\n messages: structuredClone(entry.messages),\n };\n }\n\n /**\n * Saves a session to memory.\n *\n * Creates a new session if the ID does not exist, or updates an existing session.\n * Automatically updates the updated_at timestamp and message_count.\n *\n * @param id - The unique identifier of the session.\n * @param messages - The messages to store for this session.\n * @param metadata - Optional partial metadata to merge with existing metadata.\n * @returns A promise that resolves when the save operation is complete.\n */\n async save(id: string, messages: Message[], metadata?: Partial<SessionMetadata>) {\n const now = new Date().toISOString();\n const existing = this.#sessions.get(id);\n\n this.#sessions.set(id, {\n metadata: {\n id,\n ...existing?.metadata,\n created_at: existing?.metadata.created_at ?? now,\n updated_at: now,\n message_count: messages.length,\n ...metadata,\n },\n messages: structuredClone(messages),\n });\n }\n\n /**\n * Deletes a session from memory.\n *\n * @param id - The unique identifier of the session to delete.\n * @returns A promise that resolves to true if the session was deleted, false if it was not found.\n */\n async delete(id: string) {\n return this.#sessions.delete(id);\n }\n}\n","import type { Message } from \"@simulacra-ai/core\";\nimport type { SessionMetadata, SessionStore } from \"./types.ts\";\n\n/**\n * The shape of a row in the sessions table.\n *\n * Use this as a reference when defining your drizzle table schema. The `metadata`\n * and `messages` columns should be JSON/JSONB columns (or text with JSON mode in SQLite).\n *\n * Example drizzle schema (PostgreSQL):\n * ```ts\n * import { pgTable, text, jsonb } from \"drizzle-orm/pg-core\";\n *\n * export const sessionsTable = pgTable(\"sessions\", {\n * id: text(\"id\").primaryKey(),\n * metadata: jsonb(\"metadata\").notNull(),\n * messages: jsonb(\"messages\").notNull(),\n * updated_at: text(\"updated_at\").notNull(),\n * });\n * ```\n *\n * Example drizzle schema (SQLite):\n * ```ts\n * import { sqliteTable, text } from \"drizzle-orm/sqlite-core\";\n *\n * export const sessionsTable = sqliteTable(\"sessions\", {\n * id: text(\"id\").primaryKey(),\n * metadata: text(\"metadata\", { mode: \"json\" }).notNull(),\n * messages: text(\"messages\", { mode: \"json\" }).notNull(),\n * updated_at: text(\"updated_at\").notNull(),\n * });\n * ```\n */\nexport type DrizzleSessionRow = {\n id: string;\n /** JSON-serializable value storing `Omit<SessionMetadata, \"id\">`. */\n metadata: unknown;\n /** JSON-serializable value storing `Message[]`. */\n messages: unknown;\n /** ISO 8601 timestamp — denormalized from metadata for efficient ORDER BY. */\n updated_at: string;\n}\n\n/**\n * Adapter interface that bridges `DrizzleSessionStore` with a drizzle database instance.\n *\n * Implement this interface using your own drizzle `db` and table, then pass it to\n * `DrizzleSessionStore`. This keeps drizzle out of the session package's dependencies.\n *\n * @example\n * ```ts\n * import { eq, desc } from \"drizzle-orm\";\n * import { DrizzleSessionStore, type DrizzleSessionAdapter } from \"@simulacra-ai/session\";\n *\n * const adapter: DrizzleSessionAdapter = {\n * list: () =>\n * db.select().from(sessionsTable).orderBy(desc(sessionsTable.updated_at)),\n *\n * load: async (id) => {\n * const [row] = await db\n * .select({ metadata: sessionsTable.metadata, messages: sessionsTable.messages })\n * .from(sessionsTable)\n * .where(eq(sessionsTable.id, id));\n * return row;\n * },\n *\n * upsert: (row) =>\n * db\n * .insert(sessionsTable)\n * .values(row)\n * .onConflictDoUpdate({\n * target: sessionsTable.id,\n * set: { metadata: row.metadata, messages: row.messages, updated_at: row.updated_at },\n * }),\n *\n * delete: async (id) => {\n * const result = await db.delete(sessionsTable).where(eq(sessionsTable.id, id));\n * return result.rowCount > 0;\n * },\n * };\n *\n * const store = new DrizzleSessionStore(adapter);\n * ```\n */\nexport type DrizzleSessionAdapter = {\n /**\n * Returns all session rows, sorted by `updated_at` descending (most recent first).\n */\n list(): Promise<DrizzleSessionRow[]>;\n\n /**\n * Returns the metadata and messages for a single session, or undefined if not found.\n *\n * Only `metadata` and `messages` are required in the return value.\n */\n load(id: string): Promise<Pick<DrizzleSessionRow, \"metadata\" | \"messages\"> | undefined>;\n\n /**\n * Inserts or updates a session row. On conflict with an existing `id`, the\n * metadata, messages, and updated_at columns should be updated.\n */\n upsert(row: DrizzleSessionRow): Promise<void>;\n\n /**\n * Deletes a session by id.\n *\n * @returns `true` if a row was deleted, `false` if no row with that id existed.\n */\n delete(id: string): Promise<boolean>;\n}\n\n/**\n * A database-backed implementation of `SessionStore` powered by drizzle ORM.\n *\n * Rather than importing drizzle directly, this store accepts a {@link DrizzleSessionAdapter}\n * that you implement using your own drizzle instance and table. This keeps drizzle out of\n * this package's dependencies while still providing full session persistence.\n *\n * See {@link DrizzleSessionAdapter} for an implementation example and\n * {@link DrizzleSessionRow} for the expected table schema.\n */\nexport class DrizzleSessionStore implements SessionStore {\n readonly #adapter: DrizzleSessionAdapter;\n\n constructor(adapter: DrizzleSessionAdapter) {\n this.#adapter = adapter;\n }\n\n /**\n * Lists all sessions, sorted by most recently updated first.\n *\n * @returns A promise that resolves to an array of session metadata.\n */\n async list(): Promise<SessionMetadata[]> {\n const rows = await this.#adapter.list();\n return rows.map((row) => ({\n id: row.id,\n ...(row.metadata as Omit<SessionMetadata, \"id\">),\n }));\n }\n\n /**\n * Loads a session by its ID.\n *\n * @param id - The unique identifier of the session to load.\n * @returns A promise that resolves to the session metadata and messages, or undefined if not found.\n */\n async load(id: string): Promise<{ metadata: SessionMetadata; messages: Message[] } | undefined> {\n const row = await this.#adapter.load(id);\n if (!row) {\n return undefined;\n }\n return {\n metadata: {\n id,\n ...(row.metadata as Omit<SessionMetadata, \"id\">),\n },\n messages: row.messages as Message[],\n };\n }\n\n /**\n * Saves a session with the given messages and metadata.\n *\n * Creates a new session if the ID does not exist, or updates the existing session.\n * Automatically updates the `updated_at` timestamp and `message_count`.\n *\n * @param id - The unique identifier of the session.\n * @param messages - The messages to store for this session.\n * @param metadata - Optional partial metadata to merge with existing metadata.\n * @returns A promise that resolves when the save operation is complete.\n */\n async save(id: string, messages: Message[], metadata?: Partial<SessionMetadata>): Promise<void> {\n const now = new Date().toISOString();\n const existing = await this.#adapter.load(id);\n const existing_metadata = existing?.metadata as Partial<SessionMetadata> | undefined;\n\n const merged: Omit<SessionMetadata, \"id\"> = {\n ...existing_metadata,\n created_at: existing_metadata?.created_at ?? now,\n updated_at: now,\n message_count: messages.length,\n ...metadata,\n };\n\n await this.#adapter.upsert({\n id,\n metadata: merged,\n messages,\n updated_at: now,\n });\n }\n\n /**\n * Deletes a session by its ID.\n *\n * @param id - The unique identifier of the session to delete.\n * @returns A promise that resolves to true if the session was deleted, false if it was not found.\n */\n async delete(id: string): Promise<boolean> {\n return this.#adapter.delete(id);\n }\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simulacra-ai/session",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Session persistence for the Simulacra conversation engine with pluggable file
|
|
3
|
+
"version": "0.0.6",
|
|
4
|
+
"description": "Session persistence for the Simulacra conversation engine with pluggable file, in-memory, and database storage",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
@@ -20,10 +20,20 @@
|
|
|
20
20
|
],
|
|
21
21
|
"scripts": {
|
|
22
22
|
"build": "tsup",
|
|
23
|
-
"clean": "rm -rf dist *.tsbuildinfo"
|
|
23
|
+
"clean": "rm -rf dist *.tsbuildinfo",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:watch": "vitest",
|
|
26
|
+
"test:e2e": "vitest run --config vitest.e2e.config.ts --reporter=verbose"
|
|
24
27
|
},
|
|
25
28
|
"peerDependencies": {
|
|
26
|
-
"@simulacra-ai/core": "0.0.
|
|
29
|
+
"@simulacra-ai/core": "0.0.6"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@simulacra-ai/core": "*",
|
|
33
|
+
"@types/better-sqlite3": "^7.6.12",
|
|
34
|
+
"better-sqlite3": "^11.8.1",
|
|
35
|
+
"drizzle-orm": "^0.39.3",
|
|
36
|
+
"vitest": "^3.0.0"
|
|
27
37
|
},
|
|
28
38
|
"repository": {
|
|
29
39
|
"type": "git",
|
|
@@ -34,7 +44,9 @@
|
|
|
34
44
|
"keywords": [
|
|
35
45
|
"ai",
|
|
36
46
|
"llm",
|
|
37
|
-
"agent"
|
|
47
|
+
"agent",
|
|
48
|
+
"session",
|
|
49
|
+
"simulacra"
|
|
38
50
|
],
|
|
39
51
|
"license": "MIT"
|
|
40
52
|
}
|