@asaidimu/utils-workspace 2.0.0 → 2.1.1
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 +283 -249
- package/index.d.mts +676 -204
- package/index.d.ts +676 -204
- 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
|
-
[](https://github.com/asaidimu/erp-utils/blob/main/src/workspace/LICENSE)
|
|
7
|
-
[](https://www.typescriptlang.org/)
|
|
8
|
-
[](https://vitest.dev/)
|
|
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
|
-
##
|
|
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.
|
|
|
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
|
-
|
|
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
|
-
- **
|
|
31
|
-
- **
|
|
32
|
-
- **
|
|
33
|
-
- **
|
|
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,206 +44,278 @@ Why this library? Modern AI applications need to handle large binary attachments
|
|
|
41
44
|
|
|
42
45
|
### Prerequisites
|
|
43
46
|
|
|
44
|
-
- Node.js 18+ or
|
|
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
|
|
54
|
+
# or
|
|
55
|
+
bun add @asaidimu/utils-workspace
|
|
56
|
+
# or
|
|
57
|
+
yarn add @asaidimu/utils-workspace
|
|
51
58
|
```
|
|
52
59
|
|
|
53
60
|
### Configuration
|
|
54
61
|
|
|
55
|
-
The library
|
|
62
|
+
The library does not require global configuration. You instantiate backends and stores directly.
|
|
56
63
|
|
|
57
64
|
```typescript
|
|
58
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
createWorkspaceDatabase,
|
|
67
|
+
IndexedDBBlobStorage,
|
|
68
|
+
ContentStore,
|
|
69
|
+
WorkspaceManager
|
|
70
|
+
} 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
|
+
|
|
83
|
+
// 4. WorkspaceManager (dispatches commands)
|
|
84
|
+
const workspaceManager = new WorkspaceManager(contentStore);
|
|
85
|
+
```
|
|
59
86
|
|
|
60
|
-
|
|
61
|
-
const contentBackend = new IndexedDBStorage({ dbName: 'my-workspace' });
|
|
62
|
-
await contentBackend.open();
|
|
87
|
+
### Verification
|
|
63
88
|
|
|
64
|
-
|
|
65
|
-
const blobBackend = new IndexedDBBlobStorage({ dbName: 'my-workspace-blobs' });
|
|
66
|
-
await blobBackend.open();
|
|
89
|
+
Run a quick command to ensure everything works:
|
|
67
90
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
cache: { roles: 10, preferences: 50, contextEntries: 20 },
|
|
71
|
-
flush: { maxBufferSize: 20, flushIntervalMs: 30000 },
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
const blobStore = new BlobStore(blobBackend, { eagerEviction: false });
|
|
75
|
-
await blobStore.init();
|
|
91
|
+
```typescript
|
|
92
|
+
import { createWorkspace, merge } from '@asaidimu/utils-workspace';
|
|
76
93
|
|
|
77
|
-
|
|
78
|
-
|
|
94
|
+
let workspace = createWorkspace({
|
|
95
|
+
name: "example-project",
|
|
96
|
+
owner: "user",
|
|
97
|
+
language: "en"
|
|
98
|
+
});
|
|
79
99
|
|
|
80
|
-
|
|
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
|
+
});
|
|
81
105
|
|
|
82
|
-
|
|
83
|
-
|
|
106
|
+
if (result.ok) {
|
|
107
|
+
workspace = merge(workspace, result.value);
|
|
108
|
+
console.log('Role added:', workspace.index.roles['assistant']);
|
|
109
|
+
}
|
|
84
110
|
```
|
|
85
111
|
|
|
86
112
|
---
|
|
87
113
|
|
|
88
114
|
## Usage Documentation
|
|
89
115
|
|
|
90
|
-
### Basic
|
|
91
|
-
|
|
92
|
-
Create a workspace, add a role, create a session, and append a turn.
|
|
116
|
+
### Basic Example – Create a Session, Add Turns, Build a Prompt
|
|
93
117
|
|
|
94
118
|
```typescript
|
|
95
|
-
import {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
+
}
|
|
122
192
|
|
|
123
|
-
|
|
124
|
-
const sessionCmd = {
|
|
125
|
-
type: 'session:create',
|
|
126
|
-
timestamp: new Date().toISOString(),
|
|
127
|
-
payload: { id: 's1', label: 'First Chat', role: 'assistant', topics: [] },
|
|
128
|
-
};
|
|
129
|
-
const sessionResult = await manager.dispatch(workspace, sessionCmd);
|
|
130
|
-
if (sessionResult.ok) workspace = merge(workspace, sessionResult.value);
|
|
193
|
+
demo();
|
|
131
194
|
```
|
|
132
195
|
|
|
133
|
-
###
|
|
196
|
+
### CLI Commands
|
|
134
197
|
|
|
135
|
-
|
|
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:
|
|
136
199
|
|
|
137
|
-
|
|
138
|
-
|
|
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` |
|
|
139
208
|
|
|
140
|
-
|
|
141
|
-
const openResult = await sessionManager.open(workspace, 's1');
|
|
142
|
-
if (!openResult.ok) throw new Error('Failed to open session');
|
|
209
|
+
### API Reference
|
|
143
210
|
|
|
144
|
-
|
|
211
|
+
#### Core Classes
|
|
145
212
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
parent: null,
|
|
154
|
-
};
|
|
155
|
-
const addResult = await session.addTurn(workspace, turn);
|
|
156
|
-
if (addResult.ok) workspace = merge(workspace, addResult.value);
|
|
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.
|
|
157
220
|
|
|
158
|
-
|
|
159
|
-
await session.flush();
|
|
221
|
+
#### Key Types
|
|
160
222
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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.
|
|
164
228
|
|
|
165
|
-
###
|
|
229
|
+
### Configuration Examples
|
|
166
230
|
|
|
167
|
-
|
|
168
|
-
import { BlobStore, MemoryBlobStorage } from '@asaidimu/utils-workspace';
|
|
231
|
+
#### Customising Token Estimator
|
|
169
232
|
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const registerResult = await blobStore.register(data, 'text/plain', 'readme.txt');
|
|
176
|
-
if (registerResult.ok) {
|
|
177
|
-
const blobRef = registerResult.value;
|
|
178
|
-
// Use blobRef in a turn or context block
|
|
179
|
-
}
|
|
233
|
+
```typescript
|
|
234
|
+
const promptBuilder = new PromptBuilder({
|
|
235
|
+
blobResolver: contentStore.getBlobResolver(),
|
|
236
|
+
planner: new DefaultTokenPlanner() // or extend
|
|
237
|
+
});
|
|
180
238
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
console.log(new TextDecoder().decode(resolved.data));
|
|
239
|
+
await promptBuilder.build(session, {
|
|
240
|
+
tokenBudget: {
|
|
241
|
+
total: 8000,
|
|
242
|
+
estimator: (text: string) => Math.ceil(text.length / 3), // custom
|
|
243
|
+
blobTokensPerKB: 0.5
|
|
187
244
|
}
|
|
188
|
-
}
|
|
245
|
+
});
|
|
189
246
|
```
|
|
190
247
|
|
|
191
|
-
|
|
248
|
+
#### Using a Different Context Retriever
|
|
192
249
|
|
|
193
|
-
`
|
|
250
|
+
Implement `ContextRetriever` interface:
|
|
194
251
|
|
|
195
252
|
```typescript
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const effective = await manager.resolveSession(workspace, 's1');
|
|
203
|
-
if (!effective.ok) throw new Error('Failed to resolve session');
|
|
253
|
+
class MyRetriever implements ContextRetriever {
|
|
254
|
+
rank(input: ContextRankingInput): Context[] {
|
|
255
|
+
// custom logic
|
|
256
|
+
}
|
|
257
|
+
}
|
|
204
258
|
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
providerId: 'anthropic', // for remote blobs
|
|
259
|
+
const promptBuilder = new PromptBuilder({
|
|
260
|
+
blobResolver: contentStore.getBlobResolver(),
|
|
261
|
+
retriever: new MyRetriever()
|
|
209
262
|
});
|
|
210
|
-
|
|
211
|
-
console.log(prompt.system.instructions);
|
|
212
|
-
console.log(prompt.transcript.turns);
|
|
213
|
-
console.log(prompt.warnings);
|
|
214
263
|
```
|
|
215
264
|
|
|
216
265
|
### Common Use Cases
|
|
217
266
|
|
|
218
|
-
#### 1.
|
|
267
|
+
#### 1. Upload an Image and Attach to a Turn
|
|
219
268
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
269
|
+
```typescript
|
|
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
|
|
224
279
|
|
|
225
|
-
|
|
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);
|
|
287
|
+
```
|
|
226
288
|
|
|
227
|
-
|
|
228
|
-
- Use `session:topics:add` command to associate topics with a session.
|
|
229
|
-
- `resolveSession` automatically pulls in context entries that match those topics.
|
|
289
|
+
#### 2. Edit a Previous Turn (Versioning)
|
|
230
290
|
|
|
231
|
-
|
|
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
|
+
```
|
|
232
297
|
|
|
233
|
-
|
|
234
|
-
- Each branch maintains its own head pointer.
|
|
235
|
-
- Fork an entire session with the `session:fork` command.
|
|
298
|
+
#### 3. Branch a Conversation
|
|
236
299
|
|
|
237
|
-
|
|
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.
|
|
309
|
+
```
|
|
238
310
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
311
|
+
#### 4. Garbage Collect Unreferenced Blobs
|
|
312
|
+
|
|
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();
|
|
318
|
+
```
|
|
244
319
|
|
|
245
320
|
---
|
|
246
321
|
|
|
@@ -248,83 +323,45 @@ console.log(prompt.warnings);
|
|
|
248
323
|
|
|
249
324
|
### Core Components
|
|
250
325
|
|
|
251
|
-
-
|
|
252
|
-
-
|
|
253
|
-
-
|
|
254
|
-
-
|
|
255
|
-
-
|
|
256
|
-
-
|
|
257
|
-
-
|
|
258
|
-
- **
|
|
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`.
|
|
259
335
|
|
|
260
336
|
### Data Flow
|
|
261
337
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
1. Clone the repository:
|
|
283
|
-
```bash
|
|
284
|
-
git clone https://github.com/asaidimu/erp-utils.git
|
|
285
|
-
cd erp-utils/src/workspace
|
|
286
|
-
```
|
|
287
|
-
2. Install dependencies:
|
|
288
|
-
```bash
|
|
289
|
-
npm install
|
|
290
|
-
```
|
|
291
|
-
3. Build the project:
|
|
292
|
-
```bash
|
|
293
|
-
npm run build
|
|
294
|
-
```
|
|
295
|
-
|
|
296
|
-
### Scripts
|
|
297
|
-
|
|
298
|
-
| Script | Description |
|
|
299
|
-
|----------------------|-------------------------------------------|
|
|
300
|
-
| `npm test` | Run tests once (Vitest) |
|
|
301
|
-
| `npm run test:watch` | Run tests in watch mode |
|
|
302
|
-
| `npm run test:browser` | Run tests in a browser environment |
|
|
303
|
-
|
|
304
|
-
### Testing
|
|
305
|
-
|
|
306
|
-
The test suite uses `vitest` and `fake-indexeddb/auto` to simulate IndexedDB. Run the tests with:
|
|
307
|
-
|
|
308
|
-
```bash
|
|
309
|
-
npm test
|
|
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()
|
|
310
357
|
```
|
|
311
358
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
### Contributing Guidelines
|
|
315
|
-
|
|
316
|
-
- **Branching**: Use feature branches off `main`.
|
|
317
|
-
- **Commit messages**: Follow conventional commits (e.g., `feat: add new retriever`).
|
|
318
|
-
- **Pull requests**: Ensure tests pass and add new tests for changes.
|
|
319
|
-
- **Code style**: The project uses Prettier and ESLint. Run formatting before committing.
|
|
320
|
-
|
|
321
|
-
### Issue Reporting
|
|
359
|
+
### Extension Points
|
|
322
360
|
|
|
323
|
-
|
|
324
|
-
-
|
|
325
|
-
-
|
|
326
|
-
-
|
|
327
|
-
- Version of the library and environment (Node.js / browser).
|
|
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.
|
|
328
365
|
|
|
329
366
|
---
|
|
330
367
|
|
|
@@ -332,30 +369,27 @@ Please open an issue on [GitHub](https://github.com/asaidimu/erp-utils/issues) w
|
|
|
332
369
|
|
|
333
370
|
### Troubleshooting
|
|
334
371
|
|
|
335
|
-
|
|
|
336
|
-
|
|
337
|
-
| `
|
|
338
|
-
|
|
|
339
|
-
|
|
|
340
|
-
|
|
|
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. |
|
|
341
378
|
|
|
342
379
|
### FAQ
|
|
343
380
|
|
|
344
|
-
**Q:
|
|
345
|
-
A: `
|
|
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.
|
|
346
383
|
|
|
347
|
-
**Q:
|
|
348
|
-
A:
|
|
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.
|
|
349
386
|
|
|
350
|
-
**Q:
|
|
351
|
-
A:
|
|
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.
|
|
352
389
|
|
|
353
|
-
**Q:
|
|
354
|
-
A:
|
|
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.
|
|
355
392
|
|
|
356
393
|
### License
|
|
357
|
-
This project is licensed under the [MIT License](https://github.com/asaidimu/erp-utils/blob/main/LICENSE.md).
|
|
358
|
-
|
|
359
|
-
### Acknowledgments
|
|
360
394
|
|
|
361
|
-
|
|
395
|
+
[MIT](https://github.com/asaidimu/erp-utils/blob/main/LICENSE) © Saidimu
|