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