convo-tree 0.1.0 → 0.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.
Files changed (2) hide show
  1. package/README.md +674 -74
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -2,130 +2,730 @@
2
2
 
3
3
  Tree-structured conversation state manager for branching chats.
4
4
 
5
- Manages a tree of conversation nodes where each node holds a message (role + content). You can branch at any point, navigate branches, undo/redo, prune subtrees, and serialize the full state.
5
+ [![npm version](https://img.shields.io/npm/v/convo-tree.svg)](https://www.npmjs.com/package/convo-tree)
6
+ [![npm downloads](https://img.shields.io/npm/dt/convo-tree.svg)](https://www.npmjs.com/package/convo-tree)
7
+ [![license](https://img.shields.io/npm/l/convo-tree.svg)](https://github.com/SiluPanda/convo-tree/blob/master/LICENSE)
8
+ [![node](https://img.shields.io/node/v/convo-tree.svg)](https://www.npmjs.com/package/convo-tree)
6
9
 
7
- ## Install
10
+ `convo-tree` models a conversation as a rooted tree where each node holds a message (system, user, assistant, or tool), children represent alternative continuations from the same point, and any root-to-leaf path is one complete linear conversation. The core metaphor is git: `fork()` is `git branch`, `switchTo()` is `git checkout`, `getActivePath()` is `git log --first-parent`, and `prune()` is `git branch -D`.
11
+
12
+ The package is a pure data structure with zero runtime dependencies and no network I/O. It manages the tree; the caller manages LLM interactions. Extract the active path with `getActivePath()`, send it to any LLM provider, and add the response back with `addMessage()`.
13
+
14
+ ## Installation
8
15
 
9
16
  ```bash
10
17
  npm install convo-tree
11
18
  ```
12
19
 
20
+ Requires Node.js 18 or later.
21
+
13
22
  ## Quick Start
14
23
 
15
24
  ```typescript
16
- import { createConversationTree } from 'convo-tree'
25
+ import { createConversationTree } from 'convo-tree';
26
+
27
+ // Create a tree with an automatic system prompt root node
28
+ const tree = createConversationTree({
29
+ systemPrompt: 'You are a helpful assistant.',
30
+ });
31
+
32
+ // Build a conversation by appending messages
33
+ tree.addMessage('user', 'Hello!');
34
+ tree.addMessage('assistant', 'Hi there! How can I help?');
35
+ tree.addMessage('user', 'Tell me a joke.');
36
+ tree.addMessage('assistant', 'Why did the chicken cross the road?');
37
+
38
+ // Extract the active path as a flat message array for any LLM API
39
+ const messages = tree.getActivePath();
40
+ // [
41
+ // { role: 'system', content: 'You are a helpful assistant.' },
42
+ // { role: 'user', content: 'Hello!' },
43
+ // { role: 'assistant', content: 'Hi there! How can I help?' },
44
+ // { role: 'user', content: 'Tell me a joke.' },
45
+ // { role: 'assistant', content: 'Why did the chicken cross the road?' }
46
+ // ]
47
+ ```
17
48
 
18
- const tree = createConversationTree({ systemPrompt: 'You are a helpful assistant.' })
49
+ ## Features
50
+
51
+ - **Branching conversations** -- Fork at any point to explore alternative continuations. Multiple branches coexist in a single tree structure.
52
+ - **HEAD tracking** -- A HEAD pointer tracks the current position. New messages append as children of HEAD, and HEAD advances automatically.
53
+ - **Active path extraction** -- `getActivePath()` returns a flat `Message[]` from root to HEAD, ready to send to any LLM API.
54
+ - **Undo/redo** -- Navigate backward and forward along the active path without losing history. Adding a new message after undo implicitly creates a new branch.
55
+ - **Subtree pruning** -- Remove a node and all its descendants in one operation. HEAD relocates automatically if it falls within the pruned subtree.
56
+ - **Branch labels** -- Assign human-readable labels to branches for organization (e.g., "creative approach", "model: GPT-4o").
57
+ - **Node metadata** -- Attach arbitrary key-value data to any node (model name, temperature, latency, token count).
58
+ - **Event system** -- Subscribe to `message`, `fork`, `switch`, and `prune` events for reactive UI updates and logging.
59
+ - **Serialization** -- Export the full tree state as a JSON-serializable object for persistence and restoration.
60
+ - **Zero dependencies** -- Pure data structure using only built-in Node.js APIs (`crypto.randomUUID`, `Date.now`).
61
+ - **Full TypeScript support** -- Written in TypeScript with exported type declarations.
62
+
63
+ ## API Reference
64
+
65
+ ### `createConversationTree(options?)`
19
66
 
20
- tree.addMessage('user', 'Hello!')
21
- tree.addMessage('assistant', 'Hi there! How can I help?')
22
- tree.addMessage('user', 'Tell me a joke.')
23
- tree.addMessage('assistant', 'Why did the chicken cross the road?...')
67
+ Factory function that creates and returns a `ConversationTree` instance.
24
68
 
25
- // Get the full active conversation as an array of messages
26
- const messages = tree.getActivePath()
27
- // [{ role: 'system', content: '...' }, { role: 'user', ... }, ...]
69
+ ```typescript
70
+ import { createConversationTree } from 'convo-tree';
71
+
72
+ const tree = createConversationTree({
73
+ systemPrompt: 'You are a helpful assistant.',
74
+ now: () => Date.now(),
75
+ generateId: () => crypto.randomUUID(),
76
+ });
28
77
  ```
29
78
 
30
- ## Branching
79
+ #### Options
80
+
81
+ | Option | Type | Default | Description |
82
+ |---|---|---|---|
83
+ | `systemPrompt` | `string` | `undefined` | If provided, a system-role node is created automatically as the root. |
84
+ | `treeMeta` | `Record<string, unknown>` | `undefined` | Arbitrary metadata to associate with the tree itself. |
85
+ | `now` | `() => number` | `Date.now` | Custom timestamp function used for `createdAt` on every new node. |
86
+ | `generateId` | `() => string` | `crypto.randomUUID` | Custom ID generator for node IDs. |
87
+
88
+ ---
89
+
90
+ ### `tree.addMessage(role, content, metadata?)`
91
+
92
+ Appends a new message node as a child of the current HEAD and advances HEAD to the new node. Clears the redo stack.
93
+
94
+ **Parameters:**
95
+
96
+ | Parameter | Type | Description |
97
+ |---|---|---|
98
+ | `role` | `'system' \| 'user' \| 'assistant' \| 'tool'` | The message role. |
99
+ | `content` | `string` | The message content. |
100
+ | `metadata` | `Record<string, unknown>` | Optional metadata to attach to the node. Defaults to `{}`. |
101
+
102
+ **Returns:** `ConversationNode` -- the newly created node.
31
103
 
32
104
  ```typescript
33
- const tree = createConversationTree()
34
- const root = tree.addMessage('user', 'What is the capital of France?')
35
- const responseA = tree.addMessage('assistant', 'Paris.')
105
+ const node = tree.addMessage('user', 'Hello!', { tokens: 3 });
106
+ // node.id -> unique UUID
107
+ // node.role -> 'user'
108
+ // node.content -> 'Hello!'
109
+ // node.parentId -> ID of the previous HEAD node (or null if first node)
110
+ // node.children -> []
111
+ // node.metadata -> { tokens: 3 }
112
+ // node.createdAt -> timestamp from now()
113
+ ```
114
+
115
+ When called on a node that already has children, the new message becomes a sibling, creating an implicit fork without requiring an explicit `fork()` call.
116
+
117
+ ---
118
+
119
+ ### `tree.fork(nodeId?, label?)`
36
120
 
37
- // Fork back to the user question and try a different assistant response
38
- tree.fork(root.id, 'alternate-response')
39
- tree.switchTo(root.id)
40
- const responseB = tree.addMessage('assistant', 'Paris is the capital of France.')
121
+ Marks a fork point in the tree. Does not create a new node. If `nodeId` is provided, that node becomes the fork point; otherwise the current HEAD is used. Optionally assigns a branch label to the fork point node.
41
122
 
42
- // path A: root → responseA
43
- const pathA = tree.getPathTo(responseA.id)
44
- // path B: root responseB
45
- const pathB = tree.getPathTo(responseB.id)
123
+ **Parameters:**
124
+
125
+ | Parameter | Type | Description |
126
+ |---|---|---|
127
+ | `nodeId` | `string` | Optional. The node ID to fork from. Defaults to the current HEAD. |
128
+ | `label` | `string` | Optional. A human-readable label to assign to the fork point node. |
129
+
130
+ **Returns:** `Branch` -- an object with `forkPointId` and optional `label`.
131
+
132
+ **Throws:** `InvalidOperationError` if the tree is empty. `NodeNotFoundError` if `nodeId` does not exist.
133
+
134
+ ```typescript
135
+ const branch = tree.fork(someNode.id, 'alternate-response');
136
+ // branch.forkPointId -> someNode.id
137
+ // branch.label -> 'alternate-response'
46
138
  ```
47
139
 
48
- ## Undo / Redo
140
+ After calling `fork()`, use `switchTo()` to move HEAD to the fork point, then call `addMessage()` to diverge from the original path.
141
+
142
+ ---
143
+
144
+ ### `tree.switchTo(nodeId)`
145
+
146
+ Moves HEAD to any existing node in the tree, changing the active path to the root-to-node path.
147
+
148
+ **Parameters:**
149
+
150
+ | Parameter | Type | Description |
151
+ |---|---|---|
152
+ | `nodeId` | `string` | The ID of the node to switch to. |
153
+
154
+ **Returns:** `void`
155
+
156
+ **Throws:** `NodeNotFoundError` if the node does not exist.
157
+
158
+ ```typescript
159
+ tree.switchTo(earlierNode.id);
160
+ // HEAD is now at earlierNode
161
+ // getActivePath() returns root -> ... -> earlierNode
162
+ ```
163
+
164
+ ---
165
+
166
+ ### `tree.getActivePath()`
167
+
168
+ Returns the linear message array from root to the current HEAD. The returned array is suitable for direct use with any LLM chat completion API.
169
+
170
+ **Returns:** `Message[]` -- an array of `{ role, content, ...metadata }` objects. Returns an empty array if the tree is empty.
49
171
 
50
172
  ```typescript
51
- const tree = createConversationTree()
52
- tree.addMessage('user', 'First message')
53
- tree.addMessage('assistant', 'Second message')
173
+ const messages = tree.getActivePath();
174
+ // messages[0].role -> 'system' (if systemPrompt was set)
175
+ // messages[0].content -> 'You are a helpful assistant.'
176
+ ```
177
+
178
+ Metadata fields are spread into the message object. For example, if a node has `metadata: { tokens: 5 }`, the corresponding message will include `tokens: 5` alongside `role` and `content`.
179
+
180
+ ---
181
+
182
+ ### `tree.getPathTo(nodeId)`
183
+
184
+ Returns the linear message array from root to the specified node, without changing HEAD.
185
+
186
+ **Parameters:**
187
+
188
+ | Parameter | Type | Description |
189
+ |---|---|---|
190
+ | `nodeId` | `string` | The ID of the target node. |
191
+
192
+ **Returns:** `Message[]`
54
193
 
55
- tree.undo() // head moves back to 'First message'
56
- tree.redo() // head moves forward to 'Second message'
194
+ **Throws:** `NodeNotFoundError` if the node does not exist.
195
+
196
+ ```typescript
197
+ const pathA = tree.getPathTo(responseA.id);
198
+ const pathB = tree.getPathTo(responseB.id);
199
+ // Compare two branch paths without switching HEAD
57
200
  ```
58
201
 
59
- Adding a new message after an undo clears the redo stack.
202
+ ---
60
203
 
61
- ## Prune
204
+ ### `tree.undo()`
205
+
206
+ Moves HEAD to its parent node, pushing the current HEAD onto the redo stack. Returns the new HEAD node, or `null` if HEAD is already at the root or the tree is empty.
207
+
208
+ **Returns:** `ConversationNode | null`
62
209
 
63
210
  ```typescript
64
- const tree = createConversationTree()
65
- const n1 = tree.addMessage('user', 'Root')
66
- const n2 = tree.addMessage('assistant', 'Child')
67
- tree.addMessage('user', 'Grandchild')
211
+ tree.addMessage('user', 'First');
212
+ tree.addMessage('assistant', 'Second');
213
+
214
+ const previous = tree.undo();
215
+ // previous.content -> 'First'
216
+ // tree.getHead().content -> 'First'
217
+ ```
218
+
219
+ ---
220
+
221
+ ### `tree.redo()`
222
+
223
+ Restores the most recently undone node by popping the redo stack and advancing HEAD. Returns the restored node, or `null` if the redo stack is empty or invalid.
68
224
 
69
- // Remove n2 and all its descendants (2 nodes total)
70
- const removed = tree.prune(n2.id) // returns 2
225
+ The redo stack is validated: the node to redo must be a child of the current HEAD. If the tree structure has changed (e.g., via `addMessage()` or `prune()`), the redo stack is cleared.
226
+
227
+ **Returns:** `ConversationNode | null`
228
+
229
+ ```typescript
230
+ tree.undo();
231
+ const restored = tree.redo();
232
+ // HEAD is back at the node that was undone
71
233
  ```
72
234
 
73
- ## Events
235
+ Adding a new message after `undo()` clears the redo stack, creating an implicit new branch from the undo point.
236
+
237
+ ---
238
+
239
+ ### `tree.getHead()`
240
+
241
+ Returns the current HEAD node, or `null` if the tree is empty.
242
+
243
+ **Returns:** `ConversationNode | null`
74
244
 
75
245
  ```typescript
76
- const tree = createConversationTree()
246
+ const head = tree.getHead();
247
+ if (head) {
248
+ console.log(head.role, head.content);
249
+ }
250
+ ```
77
251
 
78
- const unsub = tree.on('message', (node) => {
79
- console.log('New message:', node.role, node.content)
80
- })
252
+ ---
253
+
254
+ ### `tree.getNode(nodeId)`
255
+
256
+ Retrieves any node in the tree by its ID.
81
257
 
82
- tree.addMessage('user', 'Hello') // triggers the handler
258
+ **Parameters:**
83
259
 
84
- unsub() // stop listening
260
+ | Parameter | Type | Description |
261
+ |---|---|---|
262
+ | `nodeId` | `string` | The ID of the node to retrieve. |
263
+
264
+ **Returns:** `ConversationNode | undefined`
265
+
266
+ ```typescript
267
+ const node = tree.getNode('some-uuid');
268
+ if (node) {
269
+ console.log(node.children.length, 'children');
270
+ }
85
271
  ```
86
272
 
87
- Available events: `message`, `fork`, `switch`, `prune`.
273
+ ---
274
+
275
+ ### `tree.prune(nodeId)`
276
+
277
+ Removes the specified node and all of its descendants from the tree. Updates the parent's `children` array. If HEAD falls within the pruned subtree, HEAD is moved to the pruned node's parent. If the root is pruned, the tree is fully cleared.
278
+
279
+ **Parameters:**
88
280
 
89
- ## Serialize / Restore
281
+ | Parameter | Type | Description |
282
+ |---|---|---|
283
+ | `nodeId` | `string` | The ID of the node to prune. |
284
+
285
+ **Returns:** `number` -- the count of nodes removed (including the target node and all descendants).
286
+
287
+ **Throws:** `NodeNotFoundError` if the node does not exist.
90
288
 
91
289
  ```typescript
92
- const state = tree.serialize()
93
- // { version: 1, nodes: {...}, rootId: '...', headId: '...', redoStack: [...] }
290
+ const n1 = tree.addMessage('user', 'Root');
291
+ const n2 = tree.addMessage('assistant', 'Child');
292
+ tree.addMessage('user', 'Grandchild');
94
293
 
95
- // Store or transmit `state`, then reconstruct:
96
- const newTree = createConversationTree()
97
- // Replay messages from state.nodes in createdAt order to restore
294
+ const removed = tree.prune(n2.id);
295
+ // removed -> 2 (Child + Grandchild)
296
+ // HEAD automatically moves to n1
98
297
  ```
99
298
 
100
- ## API
299
+ Entries in the redo stack that reference pruned nodes are also removed.
101
300
 
102
- ### `createConversationTree(options?)`
301
+ ---
302
+
303
+ ### `tree.setLabel(nodeId, label)`
304
+
305
+ Sets or updates the branch label on a node.
306
+
307
+ **Parameters:**
103
308
 
104
- | Option | Type | Description |
309
+ | Parameter | Type | Description |
105
310
  |---|---|---|
106
- | `systemPrompt` | `string` | Optional system message added as the root node |
107
- | `now` | `() => number` | Custom timestamp function (default: `Date.now`) |
108
- | `generateId` | `() => string` | Custom ID generator (default: `randomUUID`) |
311
+ | `nodeId` | `string` | The ID of the node to label. |
312
+ | `label` | `string` | The label to assign. |
313
+
314
+ **Returns:** `void`
315
+
316
+ **Throws:** `NodeNotFoundError` if the node does not exist.
317
+
318
+ ```typescript
319
+ tree.setLabel(node.id, 'creative-approach');
320
+ // tree.getNode(node.id).branchLabel -> 'creative-approach'
321
+ ```
322
+
323
+ ---
324
+
325
+ ### `tree.clear()`
326
+
327
+ Resets the tree to an empty state. All nodes, the root, HEAD, and the redo stack are cleared.
328
+
329
+ **Returns:** `void`
330
+
331
+ ```typescript
332
+ tree.clear();
333
+ // tree.nodeCount -> 0
334
+ // tree.getHead() -> null
335
+ // tree.getActivePath() -> []
336
+ ```
337
+
338
+ ---
339
+
340
+ ### `tree.serialize()`
341
+
342
+ Exports the full tree state as a plain JSON-serializable object.
343
+
344
+ **Returns:** `TreeState`
345
+
346
+ ```typescript
347
+ const state = tree.serialize();
348
+ // {
349
+ // version: 1,
350
+ // nodes: { 'uuid-1': { ... }, 'uuid-2': { ... } },
351
+ // rootId: 'uuid-1',
352
+ // headId: 'uuid-2',
353
+ // redoStack: []
354
+ // }
355
+
356
+ // Persist to disk, database, or transmit over the network
357
+ const json = JSON.stringify(state);
358
+ ```
359
+
360
+ ---
361
+
362
+ ### `tree.nodeCount`
363
+
364
+ A readonly property returning the total number of nodes in the tree.
365
+
366
+ **Type:** `number`
367
+
368
+ ```typescript
369
+ console.log(tree.nodeCount); // 5
370
+ ```
371
+
372
+ ---
373
+
374
+ ### `tree.on(event, handler)`
109
375
 
110
- ### Methods
376
+ Subscribes to tree events. Returns an unsubscribe function.
111
377
 
112
- | Method | Returns | Description |
378
+ **Parameters:**
379
+
380
+ | Parameter | Type | Description |
381
+ |---|---|---|
382
+ | `event` | `string` | The event name: `'message'`, `'fork'`, `'switch'`, or `'prune'`. |
383
+ | `handler` | `Function` | The callback invoked when the event fires. |
384
+
385
+ **Returns:** `() => void` -- call this function to unsubscribe.
386
+
387
+ #### Events
388
+
389
+ | Event | Payload | Fires when |
113
390
  |---|---|---|
114
- | `addMessage(role, content, metadata?)` | `ConversationNode` | Append a message to the current head |
115
- | `fork(nodeId?, label?)` | `Branch` | Mark a fork point (defaults to current head) |
116
- | `switchTo(nodeId)` | `void` | Move head to any existing node |
117
- | `getActivePath()` | `Message[]` | Messages from root to current head |
118
- | `getPathTo(nodeId)` | `Message[]` | Messages from root to the specified node |
119
- | `undo()` | `ConversationNode \| null` | Move head to parent |
120
- | `redo()` | `ConversationNode \| null` | Re-apply the last undone message |
121
- | `getHead()` | `ConversationNode \| null` | Current head node |
122
- | `getNode(nodeId)` | `ConversationNode \| undefined` | Retrieve any node by ID |
123
- | `prune(nodeId)` | `number` | Remove a node and all descendants; returns count |
124
- | `setLabel(nodeId, label)` | `void` | Set a branch label on a node |
125
- | `clear()` | `void` | Reset the tree to empty state |
126
- | `serialize()` | `TreeState` | Export full tree state |
127
- | `on(event, handler)` | `() => void` | Subscribe to events; returns unsubscribe fn |
128
- | `nodeCount` | `number` | Total number of nodes in the tree |
391
+ | `message` | `ConversationNode` | `addMessage()` creates a new node. |
392
+ | `fork` | `Branch` | `fork()` is called. |
393
+ | `switch` | `string` (nodeId) | `switchTo()` moves HEAD. |
394
+ | `prune` | `{ nodeId: string, count: number }` | `prune()` removes nodes. |
395
+
396
+ ```typescript
397
+ const unsub = tree.on('message', (node) => {
398
+ console.log('New message:', node.role, node.content);
399
+ });
400
+
401
+ tree.addMessage('user', 'Hello'); // triggers handler
402
+
403
+ unsub(); // stop listening
404
+ tree.addMessage('user', 'World'); // handler is NOT called
405
+ ```
406
+
407
+ ## Types
408
+
409
+ All types are exported from the package entry point.
410
+
411
+ ```typescript
412
+ import type {
413
+ ConversationNode,
414
+ ConversationTree,
415
+ ConversationTreeOptions,
416
+ Branch,
417
+ Message,
418
+ TreeState,
419
+ } from 'convo-tree';
420
+ ```
421
+
422
+ ### `ConversationNode`
423
+
424
+ ```typescript
425
+ interface ConversationNode {
426
+ id: string;
427
+ role: 'system' | 'user' | 'assistant' | 'tool';
428
+ content: string;
429
+ parentId: string | null;
430
+ children: string[];
431
+ createdAt: number;
432
+ metadata: Record<string, unknown>;
433
+ branchLabel?: string;
434
+ }
435
+ ```
436
+
437
+ ### `Branch`
438
+
439
+ ```typescript
440
+ interface Branch {
441
+ forkPointId: string;
442
+ label?: string;
443
+ }
444
+ ```
445
+
446
+ ### `Message`
447
+
448
+ ```typescript
449
+ interface Message {
450
+ role: string;
451
+ content: string;
452
+ [k: string]: unknown;
453
+ }
454
+ ```
455
+
456
+ ### `TreeState`
457
+
458
+ ```typescript
459
+ interface TreeState {
460
+ nodes: Record<string, ConversationNode>;
461
+ rootId: string | null;
462
+ headId: string | null;
463
+ redoStack: string[];
464
+ version: 1;
465
+ }
466
+ ```
467
+
468
+ ### `ConversationTreeOptions`
469
+
470
+ ```typescript
471
+ interface ConversationTreeOptions {
472
+ systemPrompt?: string;
473
+ treeMeta?: Record<string, unknown>;
474
+ now?: () => number;
475
+ generateId?: () => string;
476
+ }
477
+ ```
478
+
479
+ ## Configuration
480
+
481
+ ### Custom ID Generator
482
+
483
+ Supply a deterministic ID generator for reproducible tests or when UUIDs are not desired.
484
+
485
+ ```typescript
486
+ let counter = 0;
487
+ const tree = createConversationTree({
488
+ generateId: () => `msg-${++counter}`,
489
+ });
490
+
491
+ const n1 = tree.addMessage('user', 'Hello');
492
+ // n1.id -> 'msg-1'
493
+ ```
494
+
495
+ ### Custom Timestamp
496
+
497
+ Supply a custom clock for deterministic timestamps in tests or when using a different time source.
498
+
499
+ ```typescript
500
+ const tree = createConversationTree({
501
+ now: () => 1700000000000,
502
+ });
503
+
504
+ const node = tree.addMessage('user', 'Hello');
505
+ // node.createdAt -> 1700000000000
506
+ ```
507
+
508
+ ## Error Handling
509
+
510
+ `convo-tree` exports three error classes, all extending from `ConvoTreeError`.
511
+
512
+ ```typescript
513
+ import {
514
+ ConvoTreeError,
515
+ NodeNotFoundError,
516
+ InvalidOperationError,
517
+ } from 'convo-tree';
518
+ ```
519
+
520
+ ### `ConvoTreeError`
521
+
522
+ Base error class. Has a `code` property (string) for programmatic error handling.
523
+
524
+ ```typescript
525
+ try {
526
+ tree.switchTo('nonexistent');
527
+ } catch (err) {
528
+ if (err instanceof ConvoTreeError) {
529
+ console.log(err.code); // 'NODE_NOT_FOUND'
530
+ }
531
+ }
532
+ ```
533
+
534
+ ### `NodeNotFoundError`
535
+
536
+ Thrown when an operation references a node ID that does not exist in the tree. Has a `nodeId` property indicating which ID was not found.
537
+
538
+ - **Code:** `'NODE_NOT_FOUND'`
539
+ - **Thrown by:** `switchTo()`, `getPathTo()`, `prune()`, `setLabel()`, `fork()` (when `nodeId` is provided)
540
+
541
+ ```typescript
542
+ try {
543
+ tree.getPathTo('does-not-exist');
544
+ } catch (err) {
545
+ if (err instanceof NodeNotFoundError) {
546
+ console.log(err.nodeId); // 'does-not-exist'
547
+ }
548
+ }
549
+ ```
550
+
551
+ ### `InvalidOperationError`
552
+
553
+ Thrown when an operation is structurally invalid given the current tree state.
554
+
555
+ - **Code:** `'INVALID_OPERATION'`
556
+ - **Thrown by:** `fork()` when called on an empty tree
557
+
558
+ ```typescript
559
+ const emptyTree = createConversationTree();
560
+ try {
561
+ emptyTree.fork();
562
+ } catch (err) {
563
+ if (err instanceof InvalidOperationError) {
564
+ console.log(err.message); // 'Cannot fork an empty tree'
565
+ }
566
+ }
567
+ ```
568
+
569
+ ## Advanced Usage
570
+
571
+ ### Branching Conversations
572
+
573
+ Fork at any point to explore alternative continuations, then switch between branches.
574
+
575
+ ```typescript
576
+ const tree = createConversationTree();
577
+ const question = tree.addMessage('user', 'What is the capital of France?');
578
+ const responseA = tree.addMessage('assistant', 'Paris.');
579
+
580
+ // Fork back to the question and try a different response
581
+ tree.fork(question.id, 'detailed-response');
582
+ tree.switchTo(question.id);
583
+ const responseB = tree.addMessage('assistant', 'The capital of France is Paris.');
584
+
585
+ // Extract each branch independently
586
+ const pathA = tree.getPathTo(responseA.id);
587
+ // [{ role: 'user', content: 'What is the capital of France?' },
588
+ // { role: 'assistant', content: 'Paris.' }]
589
+
590
+ const pathB = tree.getPathTo(responseB.id);
591
+ // [{ role: 'user', content: 'What is the capital of France?' },
592
+ // { role: 'assistant', content: 'The capital of France is Paris.' }]
593
+ ```
594
+
595
+ ### Undo/Redo with Implicit Branching
596
+
597
+ Calling `addMessage()` after `undo()` creates a new branch from the undo point and clears the redo stack.
598
+
599
+ ```typescript
600
+ const tree = createConversationTree();
601
+ tree.addMessage('user', 'First');
602
+ tree.addMessage('assistant', 'Second');
603
+ tree.addMessage('user', 'Third');
604
+
605
+ tree.undo(); // HEAD at 'Second'
606
+ tree.undo(); // HEAD at 'First'
607
+
608
+ // New message creates a branch from 'First'
609
+ tree.addMessage('assistant', 'Alternative second');
610
+ // redo() now returns null -- redo stack was cleared
611
+ ```
612
+
613
+ ### Serialization and Persistence
614
+
615
+ Serialize the tree for storage and reconstruct later.
616
+
617
+ ```typescript
618
+ // Save
619
+ const state = tree.serialize();
620
+ const json = JSON.stringify(state);
621
+ fs.writeFileSync('conversation.json', json);
622
+
623
+ // Load
624
+ const loaded = JSON.parse(fs.readFileSync('conversation.json', 'utf-8'));
625
+ // Reconstruct by creating a new tree and replaying messages
626
+ // from loaded.nodes in createdAt order
627
+ ```
628
+
629
+ ### Event-Driven Updates
630
+
631
+ Use the event system for reactive UI updates, logging, or analytics.
632
+
633
+ ```typescript
634
+ const tree = createConversationTree();
635
+
636
+ // Log all new messages
637
+ tree.on('message', (node) => {
638
+ console.log(`[${node.role}] ${node.content}`);
639
+ });
640
+
641
+ // Track branch creation
642
+ tree.on('fork', (branch) => {
643
+ console.log(`Forked at ${branch.forkPointId}: ${branch.label ?? 'unlabeled'}`);
644
+ });
645
+
646
+ // Monitor pruning
647
+ tree.on('prune', ({ nodeId, count }) => {
648
+ console.log(`Pruned ${count} nodes starting from ${nodeId}`);
649
+ });
650
+
651
+ // React to navigation
652
+ tree.on('switch', (nodeId) => {
653
+ console.log(`Switched HEAD to ${nodeId}`);
654
+ });
655
+ ```
656
+
657
+ ### Attaching Metadata
658
+
659
+ Store per-message provenance data such as model, latency, and token counts.
660
+
661
+ ```typescript
662
+ const node = tree.addMessage('assistant', 'Hello!', {
663
+ model: 'gpt-4o',
664
+ temperature: 0.7,
665
+ latencyMs: 450,
666
+ promptTokens: 128,
667
+ completionTokens: 12,
668
+ });
669
+
670
+ // Metadata is included in getActivePath() output
671
+ const messages = tree.getActivePath();
672
+ // Last message: { role: 'assistant', content: 'Hello!',
673
+ // model: 'gpt-4o', temperature: 0.7, latencyMs: 450, ... }
674
+ ```
675
+
676
+ ### Prompt A/B Testing
677
+
678
+ Fork at the same point to compare responses from different models or prompt configurations.
679
+
680
+ ```typescript
681
+ const tree = createConversationTree({
682
+ systemPrompt: 'You are a writing assistant.',
683
+ });
684
+
685
+ const prompt = tree.addMessage('user', 'Write a haiku about rain.');
686
+ const responseA = tree.addMessage('assistant', 'Gentle drops descend...');
687
+
688
+ // Fork for a second attempt
689
+ tree.fork(prompt.id, 'attempt-2');
690
+ tree.switchTo(prompt.id);
691
+ const responseB = tree.addMessage('assistant', 'Silver threads of rain...');
692
+
693
+ // Fork for a third attempt
694
+ tree.fork(prompt.id, 'attempt-3');
695
+ tree.switchTo(prompt.id);
696
+ const responseC = tree.addMessage('assistant', 'Clouds weep softly now...');
697
+
698
+ // Compare all three paths
699
+ const paths = [responseA, responseB, responseC].map((r) =>
700
+ tree.getPathTo(r.id)
701
+ );
702
+ ```
703
+
704
+ ## TypeScript
705
+
706
+ `convo-tree` is written in TypeScript and ships type declarations alongside the compiled JavaScript. All public types are exported from the package entry point.
707
+
708
+ ```typescript
709
+ import { createConversationTree } from 'convo-tree';
710
+ import type {
711
+ ConversationNode,
712
+ ConversationTree,
713
+ ConversationTreeOptions,
714
+ Branch,
715
+ Message,
716
+ TreeState,
717
+ } from 'convo-tree';
718
+ ```
719
+
720
+ The `ConversationTree` interface defines the full shape of the tree object returned by `createConversationTree()`. Use it for explicit typing when passing tree instances between functions.
721
+
722
+ ```typescript
723
+ function analyzeTree(tree: ConversationTree): void {
724
+ const path = tree.getActivePath();
725
+ const head = tree.getHead();
726
+ console.log(`${tree.nodeCount} nodes, head at ${head?.id ?? 'empty'}`);
727
+ }
728
+ ```
129
729
 
130
730
  ## License
131
731
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "convo-tree",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Tree-structured conversation state manager for branching chats",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",