@asaidimu/utils-workspace 3.0.0 → 3.1.0

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.
Files changed (6) hide show
  1. package/README.md +297 -277
  2. package/index.d.mts +566 -127
  3. package/index.d.ts +566 -127
  4. package/index.js +288 -1
  5. package/index.mjs +287 -1
  6. package/package.json +1 -1
package/README.md CHANGED
@@ -1,17 +1,22 @@
1
- # AI Workspace Manager
1
+ # AI Workspace SDK
2
2
 
3
- [![npm version](https://img.shields.io/badge/version-0.1.0-blue)](https://www.npmjs.com/package/@asaidimu/utils-workspace)
4
- [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
5
- [![build status](https://img.shields.io/badge/build-passing-brightgreen)]()
6
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.0-blue)]()
3
+ **Content‑addressed workspace and conversation management for AI applications.**
7
4
 
8
- > TypeScript library for managing AI conversation workspaces — content-addressed blob storage, turn‑based transcripts, role/persona management, and prompt assembly with token budgeting.
5
+ [![npm version](https://img.shields.io/npm/v/@asaidimu/utils-workspace)](https://www.npmjs.com/package/@asaidimu/utils-workspace)
6
+ [![license](https://img.shields.io/npm/l/@asaidimu/utils-workspace)](https://github.com/asaidimu/erp-utils/blob/main/src/workspace/LICENSE)
7
+ [![build status](https://img.shields.io/github/actions/workflow/status/asaidimu/erp-utils/ci.yml?branch=main)](https://github.com/asaidimu/erp-utils/actions)
9
8
 
10
9
  ## 📖 Table of Contents
11
10
 
12
11
  - [Overview & Features](#overview--features)
13
12
  - [Installation & Setup](#installation--setup)
14
13
  - [Usage Documentation](#usage-documentation)
14
+ - [Basic Conversation Flow](#basic-conversation-flow)
15
+ - [Turn Versioning & Editing](#turn-versioning--editing)
16
+ - [Working with Blobs (Images & Documents)](#working-with-blobs-images--documents)
17
+ - [Managing Preferences & Roles](#managing-preferences--roles)
18
+ - [Branching a Conversation](#branching-a-conversation)
19
+ - [Custom Prompt Assembly](#custom-prompt-assembly)
15
20
  - [Project Architecture](#project-architecture)
16
21
  - [Development & Contributing](#development--contributing)
17
22
  - [Additional Information](#additional-information)
@@ -20,23 +25,20 @@
20
25
 
21
26
  ## Overview & Features
22
27
 
23
- AI Workspace Core is a modular, offline‑first library designed to power AI‑assisted applications. It provides a complete workspace model where every conversation (session) is a DAG of **turns** (messages), backed by **content‑addressed blob storage** for large files (images, PDFs, etc.) and a flexible **context & preference** system scoped by topics.
28
+ The AI Workspace SDK provides a robust, offline‑first foundation for building AI‑powered applications. It models conversation sessions as directed acyclic graphs (DAGs) of turns, supports content‑addressed binary blobs (images, documents), and includes a flexible prompt assembly pipeline that respects token budgets, relevance scoring, and user preferences.
24
29
 
25
- The library separates **pure state updates** (reducer) from **asynchronous persistence** (ContentStore), making it suitable for reactive UIs (React, Vue, Svelte) and server environments alike. It includes built‑in IndexedDB and in‑memory backends, as well as a **PromptBuilder** that assembles prompts respecting token budgets, resolves blob references, and handles preference conflicts.
30
+ Unlike simple chat history arrays, this library treats conversations as versioned, branchable graphs enabling features like turn editing, branching conversations, and time‑travel navigation. All state changes go through a command/reducer pattern, making it easy to implement undo/redo, sync with backends, or build collaborative editors.
26
31
 
27
- ### Key Features
32
+ ### Key Features
28
33
 
29
- - **Blob Storage** – Content‑addressed (SHA‑256) storage with reference counting, remote ID mapping, and atomic registration. Supports `IndexedDBBlobStorage` (persistent) and `MemoryBlobStorage` (testing).
30
- - **Session & Turn DAG** – Each session maintains a directed acyclic graph of turns (messages). Turns can be edited (new version), branched, or deleted (subtree removal). Active chain resolves to a linear transcript.
31
- - **Role/Persona System** – Define reusable roles (e.g., “Analyst”, “Dev”) with a system persona and default preference IDs.
32
- - **Topicscoped Preferences & Context** – Preferences (user instructions) and context (injected knowledge) are attached to topics. The system resolves effective preferences for a session by merging role defaults with session‑specific overrides and filtering by session topics.
33
- - **Prompt Builder** – Three‑stage assembly:
34
- - **Retrieval & Ranking** – Jaccard similarity + freshness weighting selects relevant context entries.
35
- - **Planning** – Token‑aware planner respects budget, drops low‑priority items, and handles blob token estimation.
36
- - **Assembly** – Resolves blobs (inline or remote), injects synthetic turns for truncation notices and referential attachments, and outputs a ready‑to‑use `Prompt`.
37
- - **Reducer Pattern** – Pure `workspaceReducer` produces `DeepPartial<Workspace>` patches for all commands. WorkspaceManager applies side effects and merges async results (e.g., blob SHA‑256).
38
- - **Multi‑backend Persistence** – Uses `@asaidimu/anansi` Database interface. WorkspaceDatabase adapts it with core schemas (Role, Preference, Context, Session, Turn). Turn DAG is stored as flat documents; the graph is reconstructed in memory.
39
- - **Offline‑First & Eager/Lazy GC** – Blobs can be evicted eagerly (on refCount → 0) or lazily (via `gc()`). Sessions buffer turns and flush asynchronously.
34
+ - **Session‑based conversation DAG** – Turns are stored as versioned nodes with parent pointers. Branch, edit, or delete turns without losing history.
35
+ - **Turn versioning** – Each edit creates a new version of a turn; users can switch between versions or view the version history.
36
+ - **Content‑addressed blobs** – Binary data (images, PDFs, etc.) stored by SHA‑256, deduplicated automatically, with reference counting and remote ID mapping.
37
+ - **Tokenaware prompt assembly** – Plan prompts under token budgets, rank context by relevance (Jaccard + freshness), truncate gracefully, and inject summaries.
38
+ - **Role & preference system** – Each session has a role (persona + system prompt) and can override preference defaults per topic.
39
+ - **Task management** – First‑class task entities with steps, status, and topic linking – integrate with `task:proposal` blocks.
40
+ - **Pluggable storage** – Use IndexedDB (browser), Memory (testing/server), or implement your own backend via the `Database` and `BlobStorage` interfaces.
41
+ - **Command/reducer architecture** – All mutations are commands that produce patches; perfect for reactive UIs and event sourcing.
40
42
 
41
43
  ---
42
44
 
@@ -44,68 +46,62 @@ The library separates **pure state updates** (reducer) from **asynchronous persi
44
46
 
45
47
  ### Prerequisites
46
48
 
47
- - Node.js 18+ or modern browser (IndexedDB API required for persistent storage)
48
- - npm, bun, or yarn
49
+ - Node.js 18+ or modern browser
50
+ - npm or bun
49
51
 
50
- ### Install
52
+ ### Installation
53
+
54
+ Install the workspace package and its required peer dependency `@asaidimu/utils-database`:
51
55
 
52
56
  ```bash
53
- npm install @asaidimu/utils-workspace
54
- # or
55
- bun add @asaidimu/utils-workspace
57
+ npm install @asaidimu/utils-workspace @asaidimu/utils-database
56
58
  # or
57
- yarn add @asaidimu/utils-workspace
59
+ bun add @asaidimu/utils-workspace @asaidimu/utils-database
58
60
  ```
59
61
 
60
- ### Configuration
62
+ The library is written in TypeScript and ships with its own type definitions – no additional `@types/` packages required.
61
63
 
62
- The library does not require global configuration. You instantiate backends and stores directly.
64
+ ### Basic Configuration
63
65
 
64
66
  ```typescript
65
- import {
66
- createWorkspaceDatabase,
67
- IndexedDBBlobStorage,
68
- ContentStore,
69
- WorkspaceManager
67
+ import { createEventBus } from '@asaidimu/events';
68
+ import { DatabaseConnection, createEphemeralStore } from '@asaidimu/utils-database';
69
+ import {
70
+ ContentStore,
71
+ createWorkspaceDatabase,
72
+ MemoryBlobStorage,
73
+ WorkspaceManager,
74
+ SessionManager,
75
+ createSimpleWorkspace,
70
76
  } from '@asaidimu/utils-workspace';
71
- import { createIndexedDBDatabase } from '@asaidimu/utils-database'; // example IDB adapter
72
-
73
- // 1. Create database (IndexedDB)
74
- const idb = createIndexedDBDatabase({ dbName: 'my-app', version: 1 });
75
- const db = createWorkspaceDatabase(idb);
76
-
77
- // 2. Create blob storage (persistent)
78
- const blobStorage = new IndexedDBBlobStorage({ dbName: 'my-app-blobs' });
79
-
80
- // 3. ContentStore (caches, blob store, turn tree)
81
- const contentStore = await ContentStore.create(db, blobStorage);
82
77
 
83
- // 4. WorkspaceManager (dispatches commands)
84
- const workspaceManager = new WorkspaceManager(contentStore);
78
+ // 1. Setup database and blob storage
79
+ const db = createWorkspaceDatabase(
80
+ await DatabaseConnection(
81
+ { database: 'my-app', validate: true, predicates: {} },
82
+ createEphemeralStore()
83
+ )
84
+ );
85
+ const eventBus = createEventBus();
86
+ const blobStorage = new MemoryBlobStorage(); // or IndexedDBBlobStorage for browsers
87
+
88
+ // 2. Create core components
89
+ const contentStore = await ContentStore.create(db, blobStorage, eventBus);
90
+ const workspaceManager = new WorkspaceManager({ contentStore, eventBus });
91
+ const sessionManager = new SessionManager(workspaceManager, contentStore);
92
+
93
+ // 3. Initial workspace state
94
+ let currentWorkspace = createSimpleWorkspace({ name: 'My Workspace', language: 'en', actor: 'user' });
85
95
  ```
86
96
 
87
97
  ### Verification
88
98
 
89
- Run a quick command to ensure everything works:
99
+ Run a quick smoke test:
90
100
 
91
101
  ```typescript
92
- import { createWorkspace, merge } from '@asaidimu/utils-workspace';
93
-
94
- let workspace = createWorkspace({
95
- name: "example-project",
96
- owner: "user",
97
- language: "en"
98
- });
99
-
100
- const result = await workspaceManager.dispatch(workspace, {
101
- type: 'role:add',
102
- timestamp: new Date().toISOString(),
103
- payload: { name: 'assistant', label: 'Assistant', persona: 'You are helpful.', preferences: [] }
104
- });
105
-
106
- if (result.ok) {
107
- workspace = merge(workspace, result.value);
108
- console.log('Role added:', workspace.index.roles['assistant']);
102
+ const createResult = await sessionManager.create(currentWorkspace, { label: 'Test Session' });
103
+ if (createResult.ok) {
104
+ console.log('Session created:', createResult.value.session.id());
109
105
  }
110
106
  ```
111
107
 
@@ -113,208 +109,179 @@ if (result.ok) {
113
109
 
114
110
  ## Usage Documentation
115
111
 
116
- ### Basic Example – Create a Session, Add Turns, Build a Prompt
112
+ ### Basic Conversation Flow
117
113
 
118
114
  ```typescript
119
- import {
120
- emptyWorkspace, merge,
121
- WorkspaceManager, ContentStore,
122
- MemoryBlobStorage, createWorkspaceDatabase,
123
- SessionManager, PromptBuilder
124
- } from '@asaidimu/utils-workspace';
125
- import { createMemoryDatabase } from '@asaidimu/utils-database';
126
-
127
- async function demo() {
128
- // ---- Setup ----
129
- const memDb = createMemoryDatabase();
130
- const db = createWorkspaceDatabase(memDb);
131
-
132
- const blobStorage = new MemoryBlobStorage();
133
-
134
- const contentStore = await ContentStore.create(db, blobStorage);
135
-
136
- const workspaceManager = new WorkspaceManager(contentStore);
137
- const sessionManager = new SessionManager(workspaceManager, contentStore);
138
-
139
- let workspace = emptyWorkspace();
140
- const dispatch = async (cmd: any) => {
141
- const r = await workspaceManager.dispatch(workspace, cmd);
142
- if (r.ok) workspace = merge(workspace, r.value);
143
- else throw new Error(r.error.code);
144
- };
145
-
146
- // ---- Seed data ----
147
- await dispatch({
148
- type: 'role:add', timestamp: new Date().toISOString(),
149
- payload: { name: 'analyst', label: 'Analyst', persona: 'You are a financial analyst.', preferences: [] }
150
- });
151
- await dispatch({
152
- type: 'preference:add', timestamp: new Date().toISOString(),
153
- payload: { id: 'pref1', content: 'Use KES for currency', topics: ['finance'], timestamp: new Date().toISOString() }
154
- });
155
- await dispatch({
156
- type: 'context:add', timestamp: new Date().toISOString(),
157
- payload: { key: 'kb:q4', content: { kind: 'text', value: 'Q4 revenue: KES 4.2M' }, topics: ['finance'], timestamp: new Date().toISOString() }
158
- });
159
- await dispatch({
160
- type: 'session:create', timestamp: new Date().toISOString(),
161
- payload: { id: 'session-1', label: 'Earnings Call', role: 'analyst', topics: ['finance'], preferences: ['pref1'] }
162
- });
163
-
164
- // ---- Open session, add a turn ----
165
- const openResult = await sessionManager.open(workspace, 'session-1');
166
- if (!openResult.ok) throw new Error('open failed');
167
- const { session } = openResult.value;
168
-
169
- const addResult = await session.addTurn(workspace, {
170
- id: 'turn-1', version: 0, role: 'user',
171
- blocks: [{ type: 'text', text: 'What was the Q4 revenue?' }],
172
- timestamp: new Date().toISOString(), parent: null
173
- });
174
- if (addResult.ok) workspace = merge(workspace, addResult.value);
175
- await session.flush();
176
-
177
- // ---- Resolve effective session & build prompt ----
178
- const resolved = await workspaceManager.resolveSession(workspace, 'session-1');
179
- if (!resolved.ok) throw new Error('resolve failed');
180
-
181
- const promptBuilder = new PromptBuilder({ blobResolver: contentStore.getBlobResolver() });
182
- const prompt = await promptBuilder.build(resolved.value, {
183
- tokenBudget: { total: 4000 },
184
- relevanceConfig: { recentMessageWindow: 5 }
185
- });
186
-
187
- console.log(prompt.system.persona); // "You are a financial analyst."
188
- console.log(prompt.system.preferences[0].content); // "Use KES for currency"
189
- console.log(prompt.transcript.turns[0].blocks[0].text); // "What was the Q4 revenue?"
190
- console.log(prompt.budget.used); // token count
191
- }
115
+ import { TurnBuilder, merge } from '@asaidimu/utils-workspace';
116
+
117
+ // Create a session
118
+ const { session, patch } = (await sessionManager.create(currentWorkspace, { label: 'Chat' })).value;
119
+ currentWorkspace = merge(currentWorkspace, patch);
120
+
121
+ // Add a user turn
122
+ const userTurn = new TurnBuilder('user')
123
+ .addText('What is the weather like today?')
124
+ .build();
125
+ const addResult = await session.addTurn(currentWorkspace, userTurn);
126
+ currentWorkspace = merge(currentWorkspace, addResult.value);
127
+
128
+ // Resolve the effective session (includes preferences, context, transcript)
129
+ const effective = (await workspaceManager.resolveSession(currentWorkspace, session.id())).value;
130
+
131
+ // Build a prompt for an LLM
132
+ const promptBuilder = new PromptBuilder({ blobResolver: contentStore.getBlobResolver() });
133
+ const prompt = await promptBuilder.build(effective, {
134
+ tokenBudget: { total: 8000 },
135
+ relevanceConfig: { recentMessageWindow: 5, minScore: 0.3 }
136
+ });
192
137
 
193
- demo();
138
+ console.log(prompt.system.persona);
139
+ console.log(prompt.transcript.turns);
194
140
  ```
195
141
 
196
- ### CLI Commands
197
-
198
- AI Workspace Core is a **library**, not a CLI. However, you can build your own CLI around its commands. The core command types are:
199
142
 
200
- | Command Family | Example |
201
- |----------------|---------|
202
- | Role | `role:add`, `role:update`, `role:delete` |
203
- | Preference | `preference:add`, `preference:update`, `preference:delete` |
204
- | Context | `context:add`, `context:update`, `context:delete` |
205
- | Session | `session:create`, `session:fork`, `session:delete`, `session:role:switch`, `session:topics:add`, `session:preferences:override` |
206
- | Turn | `turn:add`, `turn:edit`, `turn:branch`, `turn:delete` |
207
- | Blob | `blob:register`, `blob:retain`, `blob:release`, `blob:purge`, `blob:record_remote_id` |
143
+ ### Turn Versioning & Editing
208
144
 
209
- ### API Reference
145
+ Every turn in a session is versioned. When you edit a turn, a new version is created while preserving the old one. The session’s “head” points to the latest version along the active chain, but users can switch between versions or view version history.
210
146
 
211
- #### Core Classes
147
+ **Editing a turn** – create a new version with modified content:
212
148
 
213
- - **`BlobStore`** – Manages blob registration, ref counting, remote IDs, and resolution. Created internally by `ContentStore`.
214
- - **`ContentStore`** High‑level API for all entities (Role, Preference, Context, Session, Turn) and blob operations. Caches records.
215
- - **`WorkspaceManager`** Dispatches commands, reduces state, and persists side effects. Entry point for state changes.
216
- - **`SessionManager`** Opens/closes live sessions, loads turn DAG, returns `Session` objects.
217
- - **`Session`** – Live session with methods: `addTurn`, `editTurn`, `branchFrom`, `deleteTurn`, `switchVersionLeft/Right`, `resolve`, `flush`, `dispose`.
218
- - **`TurnTree`** Low‑level DAG persistence (append, branch, delete subtree). Used by `Session` via `ContentStore`.
219
- - **`PromptBuilder`** – Assembles prompts from `EffectiveSession` with token budgeting and blob resolution.
220
-
221
- #### Key Types
222
-
223
- - `Workspace` – Contains `id`, `settings`, `project`, and `index` (in‑memory read model).
224
- - `BlobRef` – Lightweight pointer to a blob (sha256, mediaType, sizeBytes, optional filename).
225
- - `ResolvedBlob` – Either `{ kind: 'inline', data: Uint8Array }` or `{ kind: 'remote', fileId, providerId }`.
226
- - `EffectiveSession` – Resolved session with role, preferences, context, transcript, and optional instructions.
227
- - `Prompt` – Ready to send to an LLM; includes system block, context, transcript turns, budget breakdown, warnings, and conflicts.
228
-
229
- ### Configuration Examples
149
+ ```typescript
150
+ // Edit the user turn we just added
151
+ const newBlocks: ContentBlock[] = [
152
+ { id: uuid(), type: 'text', text: 'What is the weather like in Tokyo?' }
153
+ ];
154
+ const editResult = await session.editTurn(
155
+ currentWorkspace,
156
+ userTurn.id, // turn ID
157
+ newBlocks, // new content blocks
158
+ 'user' // optional role snapshot
159
+ );
160
+ if (editResult.ok) {
161
+ currentWorkspace = merge(currentWorkspace, editResult.value);
162
+ }
163
+ ```
230
164
 
231
- #### Customising Token Estimator
165
+ **Navigating versions** switch between versions of a turn (e.g., undo/redo):
232
166
 
233
167
  ```typescript
234
- const promptBuilder = new PromptBuilder({
235
- blobResolver: contentStore.getBlobResolver(),
236
- planner: new DefaultTokenPlanner() // or extend
237
- });
168
+ // Switch to the previous version of this turn
169
+ const leftResult = await session.switchVersionLeft(currentWorkspace, userTurn.id);
170
+ if (leftResult.ok) {
171
+ currentWorkspace = merge(currentWorkspace, leftResult.value);
172
+ // The session head now points to the previous version
173
+ }
238
174
 
239
- await promptBuilder.build(session, {
240
- tokenBudget: {
241
- total: 8000,
242
- estimator: (text: string) => Math.ceil(text.length / 3), // custom
243
- blobTokensPerKB: 0.5
244
- }
245
- });
175
+ // Switch to the next version (if available)
176
+ const rightResult = await session.switchVersionRight(currentWorkspace, userTurn.id);
246
177
  ```
247
178
 
248
- #### Using a Different Context Retriever
249
-
250
- Implement `ContextRetriever` interface:
179
+ **Inspecting version info** get available versions and navigation state:
251
180
 
252
181
  ```typescript
253
- class MyRetriever implements ContextRetriever {
254
- rank(input: ContextRankingInput): Context[] {
255
- // custom logic
256
- }
257
- }
258
-
259
- const promptBuilder = new PromptBuilder({
260
- blobResolver: contentStore.getBlobResolver(),
261
- retriever: new MyRetriever()
262
- });
182
+ const branchInfo = await session.branchInfo(userTurn.id);
183
+ console.log(branchInfo);
184
+ // {
185
+ // versions: [0, 1, 2], // all version numbers for this turn
186
+ // currentIndex: 1, // index of the active version
187
+ // total: 3,
188
+ // hasPrev: true,
189
+ // hasNext: true
190
+ // }
263
191
  ```
264
192
 
265
- ### Common Use Cases
193
+ **How it works under the hood** – When you call `editTurn`, the library:
194
+ 1. Loads the current version of the turn.
195
+ 2. Increments the version number.
196
+ 3. Stores a new turn document with the updated blocks.
197
+ 4. Updates the session head if the edited turn was the head.
198
+ 5. Preserves all previous versions, which remain accessible via `switchVersionLeft/Right` and appear in `branchInfo`.
266
199
 
267
- #### 1. Upload an Image and Attach to a Turn
200
+ This makes the conversation graph fully auditable and supports collaborative editing scenarios.
201
+
202
+ ### Working with Blobs (Images & Documents)
268
203
 
269
204
  ```typescript
270
- // Register blob via command (async)
271
- const registerCmd = {
205
+ // Register an image blob
206
+ const imageData = await fetch('/photo.jpg').then(r => new Uint8Array(await r.arrayBuffer()));
207
+ const registerCmd: RegisterBlob = {
272
208
  type: 'blob:register',
273
209
  timestamp: new Date().toISOString(),
274
- payload: { data: imageUint8Array, mediaType: 'image/png', filename: 'chart.png' }
275
- };
276
- const regResult = await workspaceManager.dispatch(workspace, registerCmd);
277
- if (regResult.ok) workspace = merge(workspace, regResult.value);
278
- const blobRef = regResult.value.index.blobs[sha256]; // BlobRef
279
-
280
- // Add turn referencing the blob
281
- const turn = {
282
- id: crypto.randomUUID(), version: 0, role: 'user',
283
- blocks: [{ type: 'image', ref: blobRef, altText: 'Revenue chart' }],
284
- timestamp: new Date().toISOString(), parent: null
210
+ payload: { data: imageData, mediaType: 'image/jpeg', filename: 'photo.jpg' }
285
211
  };
286
- await session.addTurn(workspace, turn);
212
+ const blobResult = await workspaceManager.dispatch(currentWorkspace, registerCmd);
213
+ if (blobResult.ok) {
214
+ const blobRef = blobResult.value.index?.blobs?.['sha256...']; // reference
215
+ // Use blobRef in an ImageBlock
216
+ const turnWithImage = new TurnBuilder('user')
217
+ .addImage(blobRef, 'A beautiful landscape')
218
+ .build();
219
+ }
287
220
  ```
288
221
 
289
- #### 2. Edit a Previous Turn (Versioning)
222
+ ### Managing Preferences & Roles
290
223
 
291
224
  ```typescript
292
- const editResult = await session.editTurn(workspace, 'turn-1', [
293
- { type: 'text', text: 'Corrected: Q4 revenue was KES 4.5M' }
294
- ]);
295
- // A new version (1) is created. Session head moves to version 1 of turn-1.
225
+ // Add a preference
226
+ const prefCmd: AddPreference = {
227
+ type: 'preference:add',
228
+ timestamp: new Date().toISOString(),
229
+ payload: {
230
+ id: crypto.randomUUID(),
231
+ content: 'Always use metric units for measurements.',
232
+ topics: ['weather', 'science'],
233
+ timestamp: new Date().toISOString()
234
+ }
235
+ };
236
+ await workspaceManager.dispatch(currentWorkspace, prefCmd);
237
+
238
+ // Create a role that uses that preference by default
239
+ const roleCmd: AddRole = {
240
+ type: 'role:add',
241
+ timestamp: new Date().toISOString(),
242
+ payload: {
243
+ name: 'scientist',
244
+ label: 'Science Assistant',
245
+ persona: 'You are a helpful science expert.',
246
+ preferences: [prefId]
247
+ }
248
+ };
249
+ await workspaceManager.dispatch(currentWorkspace, roleCmd);
296
250
  ```
297
251
 
298
- #### 3. Branch a Conversation
252
+
253
+ ### Branching a Conversation
299
254
 
300
255
  ```typescript
301
- const branchTurn = {
302
- id: crypto.randomUUID(), version: 0, role: 'user',
303
- blocks: [{ type: 'text', text: 'Let’s explore a different scenario.' }],
304
- timestamp: new Date().toISOString(),
305
- parent: { id: 'turn-1', version: 0 } // branch from original version
306
- };
307
- await session.branchFrom(workspace, branchTurn);
308
- // New head points to branchTurn, original chain remains.
256
+ // Get the current head turn
257
+ const head = session.head(currentWorkspace);
258
+ if (head) {
259
+ // Create a branch from that turn
260
+ const branchTurn = new TurnBuilder('assistant')
261
+ .withParent(head) // explicitly set parent
262
+ .addText('Let me think differently...')
263
+ .build();
264
+ const branchResult = await session.branch(currentWorkspace, branchTurn);
265
+ currentWorkspace = merge(currentWorkspace, branchResult.value);
266
+ }
309
267
  ```
310
268
 
311
- #### 4. Garbage Collect Unreferenced Blobs
269
+ ### Custom Prompt Assembly
270
+
271
+ The SDK includes a default token planner and Jaccard context retriever, but you can replace them:
312
272
 
313
273
  ```typescript
314
- // lazy eviction (default) – only delete bytes, keep record
315
- const deletedCount = await contentStore.blobs.gc();
316
- // full purge – delete bytes AND record
317
- const purgedCount = await contentStore.blobs.gcFull();
274
+ class MyCustomRetriever implements ContextRetriever {
275
+ rank(input: ContextRankingInput): Context[] {
276
+ // your ranking logic
277
+ }
278
+ }
279
+
280
+ const promptBuilder = new PromptBuilder({
281
+ blobResolver: contentStore.getBlobResolver(),
282
+ retriever: new MyCustomRetriever(),
283
+ planner: new MyTokenPlanner()
284
+ });
318
285
  ```
319
286
 
320
287
  ---
@@ -323,45 +290,80 @@ const purgedCount = await contentStore.blobs.gcFull();
323
290
 
324
291
  ### Core Components
325
292
 
326
- - **BlobStorage** Interface for raw bytes + record persistence. Implementations: `IndexedDBBlobStorage`, `MemoryBlobStorage`.
327
- - **BlobStore** – Adds ref counting, remote ID mapping, and resolution. Maintains in‑memory cache of `BlobRecord`s.
328
- - **ContentStore** – Owns `TurnTree` and `BlobStore`. Provides high‑level methods for all entities. Caches roles/preferences/context with LRU.
329
- - **TurnTree** – Flat storage of `Turn` documents (sessionId, id, version). Reconstructs DAG in memory (`buildNodeGraph`). Handles `append`, `replaceVersion`, `branch`, `deleteSubtree`, and head pointer on `SessionMeta`.
330
- - **Reducer** – Pure function `workspaceReducer(workspace, command) => DeepPartial<Workspace>`. Updates `Index` summaries (roles, preferences, context, sessions, topics, blobs).
331
- - **WorkspaceManager** Orchestrates reducer + side effects. Dispatches commands, merges patches, exposes `resolveSession`.
332
- - **SessionManager** Opens sessions by loading turn DAG via `TurnTree.buildNodeGraph()`. Returns `Session` object.
333
- - **Session** Live DAG with mutable `nodes` and dirty buffer. Provides turn mutations that update memory, buffer, and trigger flushes.
334
- - **PromptBuilder** Retrieves relevant context, plans token usage, resolves blob refs, assembles final `Prompt`.
293
+ | Component | Responsibility |
294
+ |-----------|----------------|
295
+ | `WorkspaceManager` | Dispatches commands, applies reducer, coordinates side effects (persistence, blob ops). |
296
+ | `ContentStore` | Session‑aware read/write layer with LRU caches for roles, preferences, context. Owns `TurnTree` and `BlobStore`. |
297
+ | `TurnTree` | Manages the turn DAG persistence, head pointer, branching, version switching, subtree deletion. |
298
+ | `BlobStore` | Content‑addressed blob registry (SHA‑256) with reference counting, remote ID mapping, and eviction. |
299
+ | `Session` | Thin coordinator for a single session validates existence, delegates to `TurnTree` for reads, forwards write commands to `WorkspaceManager`. |
300
+ | `SessionManager` | Creates, opens, and lists sessions. Returns `Session` objects (stateless, lazy loading). |
301
+ | `PromptBuilder` | Assembles prompts from an `EffectiveSession` using a `ContextRetriever`, `TokenPlanner`, and optional `Summarizer`. |
302
+ | `Reducer` | Pure function that validates commands and produces `DeepPartial<Workspace>` patches for the in‑memory index. |
335
303
 
336
304
  ### Data Flow
337
305
 
338
- ```
339
- Command → WorkspaceManager.dispatch()
340
- ├─ workspaceReducer() pure patch
341
- ├─ handleContentSideEffects() persist via ContentStore, return extra patch
342
- └─ merge patches → return to caller
343
-
344
- Session mutations (addTurn, editTurn, etc.):
345
- Session (in‑memory nodes + dirtyBuffer)
346
- └─ on flush: TurnTree.appendBatch(sessionId, turns, finalHead)
347
- └─ updates SessionMeta.head
348
- └─ stores Turn documents
349
-
350
- Prompt building:
351
- EffectiveSession (resolved by ContentStore)
352
- └─ PromptBuilder.build()
353
- ├─ ContextRetriever.rank()
354
- ├─ TokenPlanner.plan()
355
- ├─ BlobStore.resolveRefs()
356
- └─ PromptAssembler.assemble()
306
+ ```mermaid
307
+ graph LR
308
+ A[UI / Caller] -->|Command| B[WorkspaceManager]
309
+ B -->|Validate & Patch| C[Reducer]
310
+ C -->|Patch| D[In‑Memory Workspace]
311
+ B -->|Side Effects| E[ContentStore]
312
+ E --> F[(Database)]
313
+ E --> G[(Blob Storage)]
314
+ B -->|Emit Event| H[EventBus]
315
+ H -->|Notify| A
357
316
  ```
358
317
 
359
318
  ### Extension Points
360
319
 
361
- - **Custom BlobStorage** Implement `BlobStorage` interface to use S3, OPFS, etc.
362
- - **Custom ContextRetriever**Provide alternative relevance algorithms.
363
- - **Custom TokenPlanner**Override budget allocation logic.
364
- - **Summarizer**Inject summarizer to compress long transcripts before prompt assembly.
320
+ - **`Summarizer`**Compress transcripts when token budget is tight.
321
+ - **`ContextRetriever`**Custom ranking of context entries (e.g., vector similarity).
322
+ - **`TokenPlanner`**Decide which turns/preferences/context fit into a budget.
323
+ - **`ToolRegistry`**Register callable tools; `tool:call` commands will execute them.
324
+ - **`PermissionGuard`** – Intercept commands/tool calls for authentication/authorization.
325
+ - **`BlobStorage`** – Implement your own backend (S3, OPFS, etc.) by conforming to the interface.
326
+
327
+ ---
328
+
329
+ ## Development & Contributing
330
+
331
+ ### Development Setup
332
+
333
+ ```bash
334
+ git clone https://github.com/asaidimu/erp-utils.git
335
+ cd erp-utils/src/workspace
336
+ npm install
337
+ npm run build
338
+ ```
339
+
340
+ ### Available Scripts
341
+
342
+ | Script | Description |
343
+ |--------|-------------|
344
+ | `npm test` | Run unit tests once (Vitest) |
345
+ | `npm run test:watch` | Run tests in watch mode |
346
+ | `npm run test:browser` | Run tests in a browser environment |
347
+ | `npm run build` | Compile TypeScript to `dist/` |
348
+
349
+ ### Testing
350
+
351
+ Tests are written with [Vitest](https://vitest.dev/) and cover reducers, turn tree operations, blob reference counting, and prompt assembly. Run `npm test` to execute the suite. We aim for >85% coverage on core modules.
352
+
353
+ ### Contributing Guidelines
354
+
355
+ 1. **Fork the repository** and create a feature branch.
356
+ 2. **Follow the existing code style** (Prettier + ESLint configured).
357
+ 3. **Write tests** for any new functionality or bug fixes.
358
+ 4. **Commit messages** should follow [Conventional Commits](https://www.conventionalcommits.org/) (e.g., `feat: add branch info API`).
359
+ 5. **Open a pull request** against the `main` branch. Include a clear description and link to any related issue.
360
+
361
+ ### Issue Reporting
362
+
363
+ Report bugs or request features via [GitHub Issues](https://github.com/asaidimu/erp-utils/issues). Please include:
364
+ - Library version
365
+ - Minimal code reproduction
366
+ - Expected vs actual behavior
365
367
 
366
368
  ---
367
369
 
@@ -369,27 +371,45 @@ Prompt building:
369
371
 
370
372
  ### Troubleshooting
371
373
 
372
- | Problem | Likely cause | Solution |
373
- |---------|--------------|----------|
374
- | `Blob bytes not found locally` | Blob evicted but still referenced. | Re‑register the blob or disable eager eviction. |
375
- | `Database not open. Call open() first.` | Forgot to call `await blobStorage.open()`. | Ensure `open()` is called before any operation. |
376
- | `Constraint failed: unique index by_session_id_ver` | Duplicate (sessionId, id, version). | Check that `Turn` IDs are unique per session. |
377
- | `PromptBuilder` fails with `BLOB_ERROR` | Blob resolution failed (remote ID missing or bytes missing). | Verify remote mapping or ensure blob is still present. |
374
+ | Issue | Likely Solution |
375
+ |-------|----------------|
376
+ | `Blob bytes not found locally` | The blob was evicted because `refCount` reached zero and `eagerEviction` is true. Either re‑register the blob or disable eager eviction. |
377
+ | `Cannot delete role still referenced by sessions` | Change all sessions using that role to another role first, or delete the sessions. |
378
+ | `Turn not found when editing` | Ensure the turn ID and version exist. Use `session.turns()` to list current nodes. |
379
+ | `Prompt assembly drops all context` | Check your `tokenBudget.total` increase it, or provide a custom `estimator` that returns smaller token counts. |
378
380
 
379
381
  ### FAQ
380
382
 
381
- **Q: Can I use this library in a browser without IndexedDB?**
382
- A: Yes use `MemoryBlobStorage` and an in‑memory database adapter. However, data will not persist across page reloads.
383
+ **Q: How do I handle large files (e.g., 100MB videos)?**
384
+ A: The blob storage is content‑addressed, so each unique file is stored once. Use `registerBlob` to add it, then reference it via `BlobRef`. For very large files, consider implementing a streaming backend or using remote IDs (e.g., upload to S3 and store the fileId via `recordRemoteId`).
385
+
386
+ **Q: Can I use this library in a browser with IndexedDB?**
387
+ A: Yes – use `IndexedDBBlobStorage` and `createWorkspaceDatabase` with an IndexedDB adapter. The turn DAG and all entities are persisted locally.
383
388
 
384
- **Q: How do I export a workspace for backup?**
385
- A: Use `IndexedDBBlobStorage.exportAllBytes()` and `ContentStore` methods to dump all records. There is also a `WorkspaceBundle` type for structured export.
389
+ **Q: How do I migrate from a simple message array?**
390
+ A: Create a session, then add turns sequentially using `session.addTurn`. The `parent` field will be set automatically if you use `addTurn` (it links to the current head). For branching, you can also set `parent` manually.
386
391
 
387
- **Q: Does the library support streaming large files?**
388
- A: Not directly. Blobs are stored as complete `Uint8Array`. For very large files, consider splitting or using a remote storage provider and storing only the remote ID.
392
+ **Q: Does the SDK support real‑time collaboration?**
393
+ A: The command/reducer pattern is well‑suited for CRDTs or operational transforms. The workspace events (`workspace:changed`) can be broadcast to other clients, and commands can be merged. We plan to add built‑in sync in a future version.
389
394
 
390
- **Q: Can I use a different LLM provider’s file API?**
391
- A: Yes – after uploading a blob to a provider (e.g., Anthropic), call `recordBlobRemoteId(sha256, providerId, fileId)`. The `resolveRef` method will return a remote blob reference for that provider.
395
+ ### Changelog & Roadmap
396
+
397
+ See [CHANGELOG.md](https://github.com/asaidimu/erp-utils/blob/main/src/workspace/CHANGELOG.md) for version history.
398
+ Planned features:
399
+ - Built‑in summarizer using LLM calls
400
+ - Vector store integration for semantic context retrieval
401
+ - Real‑time collaboration via WebSocket transport
392
402
 
393
403
  ### License
394
404
 
395
- [MIT](https://github.com/asaidimu/erp-utils/blob/main/LICENSE) © Saidimu
405
+ MIT © [Saidimu](https://github.com/asaidimu). See [LICENSE](https://github.com/asaidimu/erp-utils/blob/main/src/workspace/LICENSE) for full text.
406
+
407
+ ### Acknowledgments
408
+
409
+ Built on:
410
+ - [`@asaidimu/anansi`](https://github.com/asaidimu/anansi) – schema‑based document store
411
+ - [`@asaidimu/events`](https://github.com/asaidimu/events) – typed event bus
412
+ - [`@asaidimu/utils-database`](https://github.com/asaidimu/erp-utils/tree/main/src/database) – database abstraction layer
413
+ - [`uuid`](https://github.com/uuidjs/uuid) – UUID generation
414
+
415
+ Inspired by modern AI orchestration frameworks and offline‑first application patterns.