@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 +274 -322
- package/index.d.mts +938 -426
- package/index.d.ts +938 -426
- package/index.js +1 -1
- package/index.mjs +1 -1
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
#
|
|
1
|
+
# AI Workspace Manager
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@asaidimu/utils-workspace)
|
|
4
|
+
[](LICENSE)
|
|
5
|
+
[]()
|
|
6
|
+
[]()
|
|
4
7
|
|
|
5
|
-
|
|
6
|
-
[](LICENSE)
|
|
7
|
-
[](https://github.com/asaidimu/utils-workspace/actions)
|
|
8
|
-
[](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
|
-
|
|
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
|
|
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
|
-
- **
|
|
30
|
-
- **Turn
|
|
31
|
-
- **
|
|
32
|
-
- **
|
|
33
|
-
- **Prompt
|
|
34
|
-
- **
|
|
35
|
-
- **
|
|
36
|
-
- **
|
|
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
|
+
- **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+
|
|
45
|
-
-
|
|
47
|
+
- Node.js 18+ or modern browser (IndexedDB API required for persistent storage)
|
|
48
|
+
- npm, bun, or yarn
|
|
46
49
|
|
|
47
|
-
###
|
|
50
|
+
### Install
|
|
48
51
|
|
|
49
52
|
```bash
|
|
50
53
|
npm install @asaidimu/utils-workspace
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
or
|
|
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
|
-
###
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
//
|
|
73
|
-
const
|
|
74
|
-
|
|
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
|
-
|
|
80
|
+
// 3. ContentStore (caches, blob store, turn tree)
|
|
81
|
+
const contentStore = await ContentStore.create(db, blobStorage);
|
|
107
82
|
|
|
108
|
-
|
|
83
|
+
// 4. WorkspaceManager (dispatches commands)
|
|
84
|
+
const workspaceManager = new WorkspaceManager(contentStore);
|
|
85
|
+
```
|
|
109
86
|
|
|
110
|
-
|
|
87
|
+
### Verification
|
|
111
88
|
|
|
112
|
-
|
|
113
|
-
let indexState = emptyState;
|
|
89
|
+
Run a quick command to ensure everything works:
|
|
114
90
|
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
94
|
+
let workspace = createWorkspace({
|
|
95
|
+
name: "example-project",
|
|
96
|
+
owner: "user",
|
|
97
|
+
language: "en"
|
|
98
|
+
});
|
|
133
99
|
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
174
|
-
await manager.flush(); // Persist dirty buffer to storage
|
|
175
|
-
```
|
|
114
|
+
## Usage Documentation
|
|
176
115
|
|
|
177
|
-
###
|
|
116
|
+
### Basic Example – Create a Session, Add Turns, Build a Prompt
|
|
178
117
|
|
|
179
118
|
```typescript
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
185
|
-
|
|
127
|
+
async function demo() {
|
|
128
|
+
// ---- Setup ----
|
|
129
|
+
const memDb = createMemoryDatabase();
|
|
130
|
+
const db = createWorkspaceDatabase(memDb);
|
|
186
131
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
202
|
-
|
|
203
|
-
timestamp: new Date().toISOString(),
|
|
204
|
-
payload: {
|
|
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
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
231
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
209
|
+
### API Reference
|
|
279
210
|
|
|
280
|
-
|
|
211
|
+
#### Core Classes
|
|
281
212
|
|
|
282
|
-
|
|
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
|
-
|
|
221
|
+
#### Key Types
|
|
285
222
|
|
|
286
|
-
|
|
287
|
-
-
|
|
288
|
-
-
|
|
289
|
-
-
|
|
290
|
-
-
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
248
|
+
#### Using a Different Context Retriever
|
|
319
249
|
|
|
320
|
-
|
|
250
|
+
Implement `ContextRetriever` interface:
|
|
321
251
|
|
|
322
252
|
```typescript
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
}>;
|
|
253
|
+
class MyRetriever implements ContextRetriever {
|
|
254
|
+
rank(input: ContextRankingInput): Context[] {
|
|
255
|
+
// custom logic
|
|
256
|
+
}
|
|
328
257
|
}
|
|
329
258
|
|
|
330
|
-
const
|
|
259
|
+
const promptBuilder = new PromptBuilder({
|
|
260
|
+
blobResolver: contentStore.getBlobResolver(),
|
|
261
|
+
retriever: new MyRetriever()
|
|
262
|
+
});
|
|
331
263
|
```
|
|
332
264
|
|
|
333
|
-
|
|
265
|
+
### Common Use Cases
|
|
266
|
+
|
|
267
|
+
#### 1. Upload an Image and Attach to a Turn
|
|
334
268
|
|
|
335
269
|
```typescript
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
-
|
|
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
|
-
|
|
298
|
+
#### 3. Branch a Conversation
|
|
349
299
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
357
|
-
```bash
|
|
358
|
-
npm install
|
|
359
|
-
```
|
|
311
|
+
#### 4. Garbage Collect Unreferenced Blobs
|
|
360
312
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
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
|
-
|
|
322
|
+
## Project Architecture
|
|
378
323
|
|
|
379
|
-
|
|
324
|
+
### Core Components
|
|
380
325
|
|
|
381
|
-
|
|
382
|
-
|
|
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
|
-
|
|
336
|
+
### Data Flow
|
|
386
337
|
|
|
387
|
-
```
|
|
388
|
-
|
|
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
|
-
###
|
|
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
|
-
- **
|
|
402
|
-
- **
|
|
403
|
-
- **
|
|
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
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
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
|
-
|
|
379
|
+
### FAQ
|
|
421
380
|
|
|
422
|
-
**Q:
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|