@asaidimu/utils-workspace 1.0.1 → 2.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.
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
- # @asaidimu/utils-workspace
1
+ # AI Workspace Manager
2
2
 
3
- Content-addressed workspace and conversation management for AI applications. Handles roles, preferences, context, transcripts, and binary blobs with a unified storage model.
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)]()
4
7
 
5
- [![npm version](https://img.shields.io/npm/v/@asaidimu/utils-workspace.svg)](https://www.npmjs.com/package/@asaidimu/utils-workspace)
6
- [![license](https://img.shields.io/npm/l/@asaidimu/utils-workspace.svg)](LICENSE)
7
- [![build status](https://img.shields.io/github/actions/workflow/status/asaidimu/utils-workspace/ci.yml)](https://github.com/asaidimu/utils-workspace/actions)
8
- [![typescript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
8
+ > TypeScript library for managing AI conversation workspaces — content-addressed blob storage, turn‑based transcripts, role/persona management, and prompt assembly with token budgeting.
9
9
 
10
- ## Table of Contents
10
+ ## 📖 Table of Contents
11
11
 
12
12
  - [Overview & Features](#overview--features)
13
13
  - [Installation & Setup](#installation--setup)
@@ -20,20 +20,23 @@ Content-addressed workspace and conversation management for AI applications. Han
20
20
 
21
21
  ## Overview & Features
22
22
 
23
- **@asaidimu/utils-workspace** provides a complete solution for managing conversational state in AI applications. Built on a content-addressed storage model, it handles everything from role definitions and user preferences to session transcripts and binary blobs. The library separates concerns between index state (fast, always in memory) and content storage (asynchronous, backend-agnostic), enabling efficient operation in both browser and Node.js environments.
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.
24
24
 
25
- The system implements a command-reducer pattern where state mutations are expressed as commands that produce patches. This design enables optimistic UI updates, offline-first operation, and easy persistence snapshots. Blob management uses SHA-256 content addressing for automatic deduplication, ref counting, and remote ID tracking across providers like Anthropic or OpenAI.
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.
26
26
 
27
27
  ### Key Features
28
28
 
29
- - **Content-Addressed Blob Storage** SHA-256 based deduplication, reference counting, and atomic storage with IndexedDB or memory backends
30
- - **Turn-Based Transcripts** Versioned conversation trees with branching, editing, and subtree deletion
31
- - **Rich Content Blocks** Support for text, images, documents, tool use, and thinking blocks with token estimation
32
- - **Preference & Context Management** Conflict resolution with timestamps, topic-based filtering, and relevance scoring with freshness decay
33
- - **Prompt Building** Token budget allocation, transcript summarization, and context ranking that produces provider-agnostic prompts
34
- - **Multiple Storage Backends** IndexedDB for browsers, MemoryStorage for testing and server-side use
35
- - **TypeScript First** Full type definitions with strict mode compatibility
36
- - **Offline-Ready** Dirty buffer management with configurable flush strategies and optimistic updates
29
+ - **Blob Storage** Content‑addressed (SHA256) 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
+ - **Topic‑scoped 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.
37
40
 
38
41
  ---
39
42
 
@@ -41,366 +44,324 @@ The system implements a command-reducer pattern where state mutations are expres
41
44
 
42
45
  ### Prerequisites
43
46
 
44
- - Node.js 18+ (for server-side usage) or modern browser with Web Crypto API support
45
- - TypeScript 5.0+ (optional, for type safety)
47
+ - Node.js 18+ or modern browser (IndexedDB API required for persistent storage)
48
+ - npm, bun, or yarn
46
49
 
47
- ### Installation
50
+ ### Install
48
51
 
49
52
  ```bash
50
53
  npm install @asaidimu/utils-workspace
51
- ```
52
-
53
- or with yarn:
54
-
55
- ```bash
54
+ # or
55
+ bun add @asaidimu/utils-workspace
56
+ # or
56
57
  yarn add @asaidimu/utils-workspace
57
58
  ```
58
59
 
59
- ### Basic Setup
60
+ ### Configuration
61
+
62
+ The library does not require global configuration. You instantiate backends and stores directly.
60
63
 
61
64
  ```typescript
62
- import {
63
- IndexedDBStorage,
64
- ContentStore,
65
- WorkspaceManager,
66
- PromptBuilder,
67
- MemoryBlobStorage,
68
- BlobStore
69
- merge,
65
+ import {
66
+ createWorkspaceDatabase,
67
+ IndexedDBBlobStorage,
68
+ ContentStore,
69
+ WorkspaceManager
70
70
  } from '@asaidimu/utils-workspace';
71
+ import { createIndexedDBDatabase } from '@asaidimu/utils-database'; // example IDB adapter
71
72
 
72
- // Initialize content storage (IndexedDB for persistence)
73
- const contentBackend = new IndexedDBStorage({ dbName: 'my-workspace' });
74
- await contentBackend.open();
75
-
76
- // Initialize blob storage (memory for this example, IndexedDB also available)
77
- const blobBackend = new MemoryBlobStorage();
78
- const blobStore = new BlobStore(blobBackend);
79
- await blobStore.init();
80
-
81
- // Create content store with default configuration
82
- const contentStore = new ContentStore(contentBackend);
83
-
84
- // Create workspace manager
85
- const manager = new WorkspaceManager(contentStore);
86
-
87
- // Initialize empty index state
88
- const emptyState = {
89
- format: 'aiworkspace/4.0',
90
- workspace: {
91
- id: 'ws-1',
92
- settings: { language: 'en', defaultRole: 'assistant' },
93
- project: { name: 'My Project', owner: 'user' },
94
- indexes: { sessions: {}, roles: {}, preferences: {}, context: {}, topics: {} }
95
- },
96
- roles: {},
97
- blobs: {},
98
- preferences: {},
99
- context: {},
100
- sessions: {}
101
- };
102
- ```
73
+ // 1. Create database (IndexedDB)
74
+ const idb = createIndexedDBDatabase({ dbName: 'my-app', version: 1 });
75
+ const db = createWorkspaceDatabase(idb);
103
76
 
104
- ---
77
+ // 2. Create blob storage (persistent)
78
+ const blobStorage = new IndexedDBBlobStorage({ dbName: 'my-app-blobs' });
105
79
 
106
- ## Usage Documentation
80
+ // 3. ContentStore (caches, blob store, turn tree)
81
+ const contentStore = await ContentStore.create(db, blobStorage);
107
82
 
108
- ### Basic Usage
83
+ // 4. WorkspaceManager (dispatches commands)
84
+ const workspaceManager = new WorkspaceManager(contentStore);
85
+ ```
109
86
 
110
- Create a role, session, and add a turn:
87
+ ### Verification
111
88
 
112
- ```typescript
113
- let indexState = emptyState;
89
+ Run a quick command to ensure everything works:
114
90
 
115
- // 1. Add a role
116
- const addRoleCmd = {
117
- type: 'role:add' as const,
118
- timestamp: new Date().toISOString(),
119
- payload: {
120
- id: 'role-1',
121
- name: 'assistant',
122
- label: 'Assistant',
123
- persona: 'You are a helpful assistant.',
124
- preferences: []
125
- }
126
- };
91
+ ```typescript
92
+ import { createWorkspace, merge } from '@asaidimu/utils-workspace';
127
93
 
128
- const roleResult = await manager.dispatch(indexState, addRoleCmd);
129
- if (roleResult.ok) {
130
- // Merge the patch into your state
131
- indexState = merge(indexState, roleResult.value) ;
132
- }
94
+ let workspace = createWorkspace({
95
+ name: "example-project",
96
+ owner: "user",
97
+ language: "en"
98
+ });
133
99
 
134
- // 2. Create a session
135
- const createSessionCmd = {
136
- type: 'session:create' as const,
100
+ const result = await workspaceManager.dispatch(workspace, {
101
+ type: 'role:add',
137
102
  timestamp: new Date().toISOString(),
138
- payload: {
139
- id: 'session-1',
140
- label: 'My First Conversation',
141
- role: 'assistant',
142
- topics: ['general'],
143
- preferences: []
144
- }
145
- };
103
+ payload: { name: 'assistant', label: 'Assistant', persona: 'You are helpful.', preferences: [] }
104
+ });
146
105
 
147
- const sessionResult = await manager.dispatch(indexState, createSessionCmd);
148
- if (sessionResult.ok) {
149
- indexState = merge(indexState, sessionResult.value) ;
106
+ if (result.ok) {
107
+ workspace = merge(workspace, result.value);
108
+ console.log('Role added:', workspace.index.roles['assistant']);
150
109
  }
110
+ ```
151
111
 
152
- // 3. Activate the session (enables dirty buffer for performance)
153
- await manager.activateSession(indexState, 'session-1');
154
-
155
- // 4. Add a turn
156
- const addTurnCmd = {
157
- type: 'turn:add' as const,
158
- timestamp: new Date().toISOString(),
159
- payload: {
160
- sessionId: 'session-1',
161
- turn: {
162
- id: 'turn-1',
163
- version: 0,
164
- role: 'user',
165
- blocks: [{ type: 'text', text: 'Hello, how are you?' }],
166
- timestamp: new Date().toISOString(),
167
- roleSnapshot: 'assistant',
168
- parent: null
169
- }
170
- }
171
- };
112
+ ---
172
113
 
173
- await manager.dispatch(indexState, addTurnCmd);
174
- await manager.flush(); // Persist dirty buffer to storage
175
- ```
114
+ ## Usage Documentation
176
115
 
177
- ### Registering and Using Blobs
116
+ ### Basic Example Create a Session, Add Turns, Build a Prompt
178
117
 
179
118
  ```typescript
180
- // Register a blob (e.g., an image or document)
181
- const imageData = new TextEncoder().encode('fake-image-data');
182
- const blobResult = await blobStore.register(imageData, 'image/jpeg', 'photo.jpg');
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';
183
126
 
184
- if (blobResult.ok) {
185
- const blobRef = blobResult.value;
127
+ async function demo() {
128
+ // ---- Setup ----
129
+ const memDb = createMemoryDatabase();
130
+ const db = createWorkspaceDatabase(memDb);
186
131
 
187
- // Use the blob in a turn
188
- const turnWithBlob = {
189
- id: 'turn-2',
190
- version: 0,
191
- role: 'user',
192
- blocks: [
193
- { type: 'text', text: 'What is in this image?' },
194
- { type: 'image', blob: blobRef }
195
- ],
196
- timestamp: new Date().toISOString(),
197
- roleSnapshot: 'assistant',
198
- parent: null
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);
199
144
  };
200
145
 
201
- await manager.dispatch(indexState, {
202
- type: 'turn:add',
203
- timestamp: new Date().toISOString(),
204
- payload: { sessionId: 'session-1', turn: turnWithBlob }
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'] }
205
162
  });
206
- }
207
- ```
208
-
209
- ### Building Prompts
210
-
211
- ```typescript
212
- // Resolve the session to get effective preferences, context, and transcript
213
- const resolved = await manager.resolveSession(indexState, 'session-1');
214
-
215
- if (resolved.ok) {
216
- // Resolve all blobs in the session
217
- const refs = resolved.value.transcript
218
- .flatMap(t => t.blocks.filter(b => b.type === 'image' || b.type === 'document'))
219
- .map(b => b.blob);
220
163
 
221
- const { resolved: resolvedBlobs, errors } = await blobStore.resolveRefs(refs, null);
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;
222
168
 
223
- // Build the prompt
224
- const builder = new PromptBuilder();
225
- const prompt = await builder.build(resolved.value, {
226
- resolvedBlobs,
227
- tokenBudget: { total: 4000 }
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
228
173
  });
174
+ if (addResult.ok) workspace = merge(workspace, addResult.value);
175
+ await session.flush();
229
176
 
230
- console.log(prompt.system); // System prompt with persona, preferences, context
231
- console.log(prompt.turns); // Conversation turns with resolved blobs
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
232
191
  }
233
- ```
234
192
 
235
- ### Configuration
236
-
237
- **ContentStore Configuration**
238
-
239
- ```typescript
240
- const contentStore = new ContentStore(backend, {
241
- cache: {
242
- roles: 10, // Max roles in LRU cache
243
- preferences: 50, // Max preferences in LRU cache
244
- contextEntries: 20, // Max context entries in LRU cache
245
- transcriptWindows: 3 // Max transcript windows in LRU cache
246
- },
247
- flush: {
248
- maxBufferSize: 10, // Auto-flush after 10 turns
249
- flushIntervalMs: 30000 // Auto-flush every 30 seconds
250
- },
251
- transcriptWindowSize: 20 // Default turns per window
252
- });
253
- ```
254
-
255
- **BlobStore Configuration**
256
-
257
- ```typescript
258
- const blobStore = new BlobStore(backend, {
259
- eagerEviction: false // When false, blobs with refCount 0 persist until gc()
260
- });
193
+ demo();
261
194
  ```
262
195
 
263
- **IndexedDB Configuration**
264
-
265
- ```typescript
266
- const contentBackend = new IndexedDBStorage({ dbName: 'custom-db-name' });
267
- const blobBackend = new IndexedDBBlobStorage({ dbName: 'custom-blob-db' });
268
- ```
269
-
270
- ---
271
-
272
- ## Project Architecture
196
+ ### CLI Commands
273
197
 
274
- ### Core Components
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:
275
199
 
276
- **IndexState** The in-memory representation of the workspace. Contains summaries of all entities (roles, preferences, contexts, sessions, blobs) and is designed for fast reads. State mutations produce patches that can be merged optimistically.
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` |
277
208
 
278
- **ContentStore** Manages asynchronous access to persisted content. Implements LRU caching with pinning for active sessions, dirty buffer management for turns, and maintains the distinction between index state and full content.
209
+ ### API Reference
279
210
 
280
- **WorkspaceManager** The public facade that coordinates between commands, reducer, and content persistence. Handles side effects (saving to storage) while keeping the reducer pure.
211
+ #### Core Classes
281
212
 
282
- **BlobStore** Content-addressed blob management with SHA-256 deduplication, reference counting, remote ID tracking, and resolution between inline and remote blob references.
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.
283
220
 
284
- **PromptBuilder** Transforms an EffectiveSession into a provider-agnostic prompt structure with token budgeting, preference conflict resolution, context ranking, and optional summarization.
221
+ #### Key Types
285
222
 
286
- **Storage Backends** Pluggable storage layers:
287
- - **MemoryStorage** In-memory implementation for testing
288
- - **IndexedDBStorage** Persistent browser storage for content
289
- - **MemoryBlobStorage** In-memory blob storage
290
- - **IndexedDBBlobStorage** Persistent browser storage for blobs
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.
291
228
 
292
- ### Data Flow
229
+ ### Configuration Examples
293
230
 
294
- ```
295
- Command → WorkspaceManager.dispatch()
296
-
297
- WorkspaceReducer (pure patch generation)
298
-
299
- ContentStore (side effects: save roles, preferences, turns, etc.)
300
-
301
- Storage Backend (IndexedDB / Memory)
302
- ```
231
+ #### Customising Token Estimator
303
232
 
304
- For resolution and prompt building:
233
+ ```typescript
234
+ const promptBuilder = new PromptBuilder({
235
+ blobResolver: contentStore.getBlobResolver(),
236
+ planner: new DefaultTokenPlanner() // or extend
237
+ });
305
238
 
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
+ });
306
246
  ```
307
- IndexState + SessionId → ContentStore.resolveSession()
308
- ↓ (async content loading)
309
- EffectiveSession → BlobStore.resolveRefs()
310
- ↓ (blob resolution)
311
- PromptBuilder.build()
312
- ↓ (token budgeting, summarization)
313
- Prompt (system, turns, warnings)
314
- ```
315
-
316
- ### Extension Points
317
247
 
318
- **Custom Storage Backends** Implement the `ContentStorage` or `BlobStorage` interfaces to support alternative persistence (SQLite, cloud storage, etc.)
248
+ #### Using a Different Context Retriever
319
249
 
320
- **Summarizer** — Inject a custom summarizer into `PromptBuilder` for transcript compression:
250
+ Implement `ContextRetriever` interface:
321
251
 
322
252
  ```typescript
323
- interface Summarizer {
324
- summarize(transcript: Turn[], tokenBudget: number): Promise<{
325
- summary: string;
326
- remaining: Turn[];
327
- }>;
253
+ class MyRetriever implements ContextRetriever {
254
+ rank(input: ContextRankingInput): Context[] {
255
+ // custom logic
256
+ }
328
257
  }
329
258
 
330
- const builder = new PromptBuilder(myCustomSummarizer);
259
+ const promptBuilder = new PromptBuilder({
260
+ blobResolver: contentStore.getBlobResolver(),
261
+ retriever: new MyRetriever()
262
+ });
331
263
  ```
332
264
 
333
- **Token Estimators** Override the default text estimator for provider-specific token counting:
265
+ ### Common Use Cases
266
+
267
+ #### 1. Upload an Image and Attach to a Turn
334
268
 
335
269
  ```typescript
336
- const prompt = await builder.build(session, {
337
- tokenBudget: {
338
- total: 8000,
339
- estimator: (text) => myProviderTokenCounter.count(text)
340
- }
341
- });
270
+ // Register blob via command (async)
271
+ const registerCmd = {
272
+ type: 'blob:register',
273
+ 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
285
+ };
286
+ await session.addTurn(workspace, turn);
342
287
  ```
343
288
 
344
- ---
289
+ #### 2. Edit a Previous Turn (Versioning)
345
290
 
346
- ## Development & Contributing
291
+ ```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.
296
+ ```
347
297
 
348
- ### Development Setup
298
+ #### 3. Branch a Conversation
349
299
 
350
- 1. Clone the repository:
351
- ```bash
352
- git clone https://github.com/asaidimu/utils-workspace.git
353
- cd utils-workspace
300
+ ```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.
354
309
  ```
355
310
 
356
- 2. Install dependencies:
357
- ```bash
358
- npm install
359
- ```
311
+ #### 4. Garbage Collect Unreferenced Blobs
360
312
 
361
- 3. Build the project:
362
- ```bash
363
- npm run build
313
+ ```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();
364
318
  ```
365
319
 
366
- ### Available Scripts
367
-
368
- | Script | Description |
369
- |--------|-------------|
370
- | `npm run build` | Compile TypeScript to JavaScript |
371
- | `npm run test` | Run all tests with Vitest |
372
- | `npm run test:watch` | Run tests in watch mode |
373
- | `npm run lint` | Run ESLint |
374
- | `npm run format` | Format code with Prettier |
375
- | `npm run typecheck` | Run TypeScript type checking |
320
+ ---
376
321
 
377
- ### Testing
322
+ ## Project Architecture
378
323
 
379
- The test suite uses Vitest with fake-indexeddb for browser environment simulation. Run tests with:
324
+ ### Core Components
380
325
 
381
- ```bash
382
- npm run test
383
- ```
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`.
384
335
 
385
- Coverage reports can be generated with:
336
+ ### Data Flow
386
337
 
387
- ```bash
388
- npm run test:coverage
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()
389
357
  ```
390
358
 
391
- ### Contributing Guidelines
392
-
393
- 1. **Fork and Branch** — Create a feature branch from `main`
394
- 2. **Write Tests** — Add tests for new functionality or bug fixes
395
- 3. **Update Documentation** — Keep README and JSDoc comments current
396
- 4. **Commit Convention** — Use conventional commits (feat:, fix:, docs:, etc.)
397
- 5. **Submit PR** — Open a pull request with a clear description of changes
398
-
399
- ### Issue Reporting
359
+ ### Extension Points
400
360
 
401
- - **Bug Reports** Include minimal reproduction, expected vs actual behavior, and environment details
402
- - **Feature Requests** Describe use case and expected API
403
- - **Security Issues** Email maintainers directly (see package.json for contact)
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.
404
365
 
405
366
  ---
406
367
 
@@ -408,36 +369,27 @@ npm run test:coverage
408
369
 
409
370
  ### Troubleshooting
410
371
 
411
- **Q: Blob bytes not found locally error**
412
-
413
- Blobs with refCount 0 are eligible for garbage collection. If you need them later, ensure:
414
- - `eagerEviction: false` in BlobStore config
415
- - `gc()` is not called before resolution
416
- - Register the blob again if it was evicted
417
-
418
- **Q: IndexedDB open fails with "blocked" error**
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. |
419
378
 
420
- Another browser tab has an open connection to the same database. Close the other tab or wait for it to release the connection.
379
+ ### FAQ
421
380
 
422
- **Q: Turn edits not persisting**
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.
423
383
 
424
- Ensure `flush()` is called after batch operations, or configure `flush.maxBufferSize` to auto-flush.
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.
425
386
 
426
- **Q: TypeScript errors with custom storage backends**
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.
427
389
 
428
- Import the interface types and implement all required methods:
429
-
430
- ```typescript
431
- import type { ContentStorage, BlobStorage } from '@asaidimu/utils-workspace';
432
- ```
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.
433
392
 
434
393
  ### License
435
394
 
436
- MIT © [Saidimu](https://github.com/asaidimu)
437
-
438
- See the [LICENSE](LICENSE) file for details.
439
-
440
- ---
441
-
442
- **Related Projects**
443
- - [@asaidimu/utils-store](https://github.com/asaidimu/utils-store) — Deep merge utilities and store patterns
395
+ [MIT](https://github.com/asaidimu/erp-utils/blob/main/LICENSE) © Saidimu