@deepagents/context 0.8.1 → 0.10.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/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ // packages/context/src/index.ts
2
+ import { generateId } from "ai";
3
+
1
4
  // packages/context/src/lib/context.ts
2
5
  function isFragment(data) {
3
6
  return typeof data === "object" && data !== null && "name" in data && "data" in data && typeof data.name === "string";
@@ -119,7 +122,8 @@ var ModelsRegistry = class {
119
122
  context: model.limit.context,
120
123
  output: model.limit.output,
121
124
  exceedsContext: tokens > model.limit.context
122
- }
125
+ },
126
+ fragments: []
123
127
  };
124
128
  }
125
129
  };
@@ -130,6 +134,43 @@ function getModelsRegistry() {
130
134
  }
131
135
  return _registry;
132
136
  }
137
+ async function estimate(modelId, renderer, ...fragments) {
138
+ const registry = getModelsRegistry();
139
+ await registry.load();
140
+ const input = renderer.render(fragments);
141
+ const model = registry.get(modelId);
142
+ if (!model) {
143
+ throw new Error(
144
+ `Model "${modelId}" not found. Call load() first or check model ID.`
145
+ );
146
+ }
147
+ const tokenizer = registry.getTokenizer(modelId);
148
+ const totalTokens = tokenizer.count(input);
149
+ const totalCost = totalTokens / 1e6 * model.cost.input;
150
+ const fragmentEstimates = fragments.map((fragment2) => {
151
+ const rendered = renderer.render([fragment2]);
152
+ const tokens = tokenizer.count(rendered);
153
+ const cost = tokens / 1e6 * model.cost.input;
154
+ return {
155
+ id: fragment2.id,
156
+ name: fragment2.name,
157
+ tokens,
158
+ cost
159
+ };
160
+ });
161
+ return {
162
+ model: model.id,
163
+ provider: model.provider,
164
+ tokens: totalTokens,
165
+ cost: totalCost,
166
+ limits: {
167
+ context: model.limit.context,
168
+ output: model.limit.output,
169
+ exceedsContext: totalTokens > model.limit.context
170
+ },
171
+ fragments: fragmentEstimates
172
+ };
173
+ }
133
174
 
134
175
  // packages/context/src/lib/renderers/abstract.renderer.ts
135
176
  import pluralize from "pluralize";
@@ -789,12 +830,13 @@ var ContextStore = class {
789
830
  import { DatabaseSync } from "node:sqlite";
790
831
  var STORE_DDL = `
791
832
  -- Chats table
833
+ -- createdAt/updatedAt: DEFAULT for insert, inline SET for updates
792
834
  CREATE TABLE IF NOT EXISTS chats (
793
835
  id TEXT PRIMARY KEY,
794
836
  title TEXT,
795
837
  metadata TEXT,
796
- createdAt INTEGER NOT NULL,
797
- updatedAt INTEGER NOT NULL
838
+ createdAt INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
839
+ updatedAt INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)
798
840
  );
799
841
 
800
842
  CREATE INDEX IF NOT EXISTS idx_chats_updatedAt ON chats(updatedAt);
@@ -807,7 +849,6 @@ CREATE TABLE IF NOT EXISTS messages (
807
849
  name TEXT NOT NULL,
808
850
  type TEXT,
809
851
  data TEXT NOT NULL,
810
- persist INTEGER NOT NULL DEFAULT 1,
811
852
  createdAt INTEGER NOT NULL,
812
853
  FOREIGN KEY (chatId) REFERENCES chats(id) ON DELETE CASCADE,
813
854
  FOREIGN KEY (parentId) REFERENCES messages(id)
@@ -846,13 +887,13 @@ CREATE TABLE IF NOT EXISTS checkpoints (
846
887
  CREATE INDEX IF NOT EXISTS idx_checkpoints_chatId ON checkpoints(chatId);
847
888
 
848
889
  -- FTS5 virtual table for full-text search
849
- -- Using external content mode (content='') so we manage sync manually
890
+ -- messageId/chatId/name are UNINDEXED (stored but not searchable, used for filtering/joining)
891
+ -- Only 'content' is indexed for full-text search
850
892
  CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
851
- messageId,
852
- chatId,
853
- name,
893
+ messageId UNINDEXED,
894
+ chatId UNINDEXED,
895
+ name UNINDEXED,
854
896
  content,
855
- content='',
856
897
  tokenize='porter unicode61'
857
898
  );
858
899
  `;
@@ -869,16 +910,33 @@ var SqliteContextStore = class extends ContextStore {
869
910
  // ==========================================================================
870
911
  async createChat(chat) {
871
912
  this.#db.prepare(
872
- `INSERT INTO chats (id, title, metadata, createdAt, updatedAt)
873
- VALUES (?, ?, ?, ?, ?)`
913
+ `INSERT INTO chats (id, title, metadata)
914
+ VALUES (?, ?, ?)`
874
915
  ).run(
875
916
  chat.id,
876
917
  chat.title ?? null,
877
- chat.metadata ? JSON.stringify(chat.metadata) : null,
878
- chat.createdAt,
879
- chat.updatedAt
918
+ chat.metadata ? JSON.stringify(chat.metadata) : null
880
919
  );
881
920
  }
921
+ async upsertChat(chat) {
922
+ const row = this.#db.prepare(
923
+ `INSERT INTO chats (id, title, metadata)
924
+ VALUES (?, ?, ?)
925
+ ON CONFLICT(id) DO UPDATE SET id = excluded.id
926
+ RETURNING *`
927
+ ).get(
928
+ chat.id,
929
+ chat.title ?? null,
930
+ chat.metadata ? JSON.stringify(chat.metadata) : null
931
+ );
932
+ return {
933
+ id: row.id,
934
+ title: row.title ?? void 0,
935
+ metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
936
+ createdAt: row.createdAt,
937
+ updatedAt: row.updatedAt
938
+ };
939
+ }
882
940
  async getChat(chatId) {
883
941
  const row = this.#db.prepare("SELECT * FROM chats WHERE id = ?").get(chatId);
884
942
  if (!row) {
@@ -893,7 +951,7 @@ var SqliteContextStore = class extends ContextStore {
893
951
  };
894
952
  }
895
953
  async updateChat(chatId, updates) {
896
- const setClauses = [];
954
+ const setClauses = ["updatedAt = strftime('%s', 'now') * 1000"];
897
955
  const params = [];
898
956
  if (updates.title !== void 0) {
899
957
  setClauses.push("title = ?");
@@ -903,15 +961,17 @@ var SqliteContextStore = class extends ContextStore {
903
961
  setClauses.push("metadata = ?");
904
962
  params.push(JSON.stringify(updates.metadata));
905
963
  }
906
- if (updates.updatedAt !== void 0) {
907
- setClauses.push("updatedAt = ?");
908
- params.push(updates.updatedAt);
909
- }
910
- if (setClauses.length === 0) {
911
- return;
912
- }
913
964
  params.push(chatId);
914
- this.#db.prepare(`UPDATE chats SET ${setClauses.join(", ")} WHERE id = ?`).run(...params);
965
+ const row = this.#db.prepare(
966
+ `UPDATE chats SET ${setClauses.join(", ")} WHERE id = ? RETURNING *`
967
+ ).get(...params);
968
+ return {
969
+ id: row.id,
970
+ title: row.title ?? void 0,
971
+ metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
972
+ createdAt: row.createdAt,
973
+ updatedAt: row.updatedAt
974
+ };
915
975
  }
916
976
  async listChats() {
917
977
  const rows = this.#db.prepare(
@@ -940,25 +1000,30 @@ var SqliteContextStore = class extends ContextStore {
940
1000
  // ==========================================================================
941
1001
  // Message Operations (Graph Nodes)
942
1002
  // ==========================================================================
943
- async addMessage(message) {
1003
+ async addMessage(message2) {
944
1004
  this.#db.prepare(
945
- `INSERT INTO messages (id, chatId, parentId, name, type, data, persist, createdAt)
946
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
1005
+ `INSERT INTO messages (id, chatId, parentId, name, type, data, createdAt)
1006
+ VALUES (?, ?, ?, ?, ?, ?, ?)
1007
+ ON CONFLICT(id) DO UPDATE SET
1008
+ parentId = excluded.parentId,
1009
+ name = excluded.name,
1010
+ type = excluded.type,
1011
+ data = excluded.data`
947
1012
  ).run(
948
- message.id,
949
- message.chatId,
950
- message.parentId,
951
- message.name,
952
- message.type ?? null,
953
- JSON.stringify(message.data),
954
- message.persist ? 1 : 0,
955
- message.createdAt
1013
+ message2.id,
1014
+ message2.chatId,
1015
+ message2.parentId,
1016
+ message2.name,
1017
+ message2.type ?? null,
1018
+ JSON.stringify(message2.data),
1019
+ message2.createdAt
956
1020
  );
957
- const content = typeof message.data === "string" ? message.data : JSON.stringify(message.data);
1021
+ const content = typeof message2.data === "string" ? message2.data : JSON.stringify(message2.data);
1022
+ this.#db.prepare(`DELETE FROM messages_fts WHERE messageId = ?`).run(message2.id);
958
1023
  this.#db.prepare(
959
1024
  `INSERT INTO messages_fts(messageId, chatId, name, content)
960
1025
  VALUES (?, ?, ?, ?)`
961
- ).run(message.id, message.chatId, message.name, content);
1026
+ ).run(message2.id, message2.chatId, message2.name, content);
962
1027
  }
963
1028
  async getMessage(messageId) {
964
1029
  const row = this.#db.prepare("SELECT * FROM messages WHERE id = ?").get(messageId);
@@ -972,7 +1037,6 @@ var SqliteContextStore = class extends ContextStore {
972
1037
  name: row.name,
973
1038
  type: row.type ?? void 0,
974
1039
  data: JSON.parse(row.data),
975
- persist: row.persist === 1,
976
1040
  createdAt: row.createdAt
977
1041
  };
978
1042
  }
@@ -994,7 +1058,6 @@ var SqliteContextStore = class extends ContextStore {
994
1058
  name: row.name,
995
1059
  type: row.type ?? void 0,
996
1060
  data: JSON.parse(row.data),
997
- persist: row.persist === 1,
998
1061
  createdAt: row.createdAt
999
1062
  }));
1000
1063
  }
@@ -1155,7 +1218,6 @@ var SqliteContextStore = class extends ContextStore {
1155
1218
  m.name,
1156
1219
  m.type,
1157
1220
  m.data,
1158
- m.persist,
1159
1221
  m.createdAt,
1160
1222
  fts.rank,
1161
1223
  snippet(messages_fts, 3, '<mark>', '</mark>', '...', 32) as snippet
@@ -1181,7 +1243,6 @@ var SqliteContextStore = class extends ContextStore {
1181
1243
  name: row.name,
1182
1244
  type: row.type ?? void 0,
1183
1245
  data: JSON.parse(row.data),
1184
- persist: row.persist === 1,
1185
1246
  createdAt: row.createdAt
1186
1247
  },
1187
1248
  rank: row.rank,
@@ -1305,7 +1366,7 @@ function visualizeGraph(data) {
1305
1366
  // packages/context/src/index.ts
1306
1367
  var ContextEngine = class {
1307
1368
  /** Non-message fragments (role, hints, etc.) - not persisted in graph */
1308
- #contextFragments = [];
1369
+ #fragments = [];
1309
1370
  /** Pending message fragments to be added to graph */
1310
1371
  #pendingMessages = [];
1311
1372
  #store;
@@ -1329,18 +1390,7 @@ var ContextEngine = class {
1329
1390
  if (this.#initialized) {
1330
1391
  return;
1331
1392
  }
1332
- const existingChat = await this.#store.getChat(this.#chatId);
1333
- if (existingChat) {
1334
- this.#chatData = existingChat;
1335
- } else {
1336
- const now = Date.now();
1337
- this.#chatData = {
1338
- id: this.#chatId,
1339
- createdAt: now,
1340
- updatedAt: now
1341
- };
1342
- await this.#store.createChat(this.#chatData);
1343
- }
1393
+ this.#chatData = await this.#store.upsertChat({ id: this.#chatId });
1344
1394
  const existingBranch = await this.#store.getBranch(
1345
1395
  this.#chatId,
1346
1396
  this.#branchName
@@ -1360,6 +1410,41 @@ var ContextEngine = class {
1360
1410
  }
1361
1411
  this.#initialized = true;
1362
1412
  }
1413
+ /**
1414
+ * Create a new branch from a specific message.
1415
+ * Shared logic between rewind() and btw().
1416
+ */
1417
+ async #createBranchFrom(messageId, switchTo) {
1418
+ const branches = await this.#store.listBranches(this.#chatId);
1419
+ const samePrefix = branches.filter(
1420
+ (b) => b.name === this.#branchName || b.name.startsWith(`${this.#branchName}-v`)
1421
+ );
1422
+ const newBranchName = `${this.#branchName}-v${samePrefix.length + 1}`;
1423
+ const newBranch = {
1424
+ id: crypto.randomUUID(),
1425
+ chatId: this.#chatId,
1426
+ name: newBranchName,
1427
+ headMessageId: messageId,
1428
+ isActive: false,
1429
+ createdAt: Date.now()
1430
+ };
1431
+ await this.#store.createBranch(newBranch);
1432
+ if (switchTo) {
1433
+ await this.#store.setActiveBranch(this.#chatId, newBranch.id);
1434
+ this.#branch = { ...newBranch, isActive: true };
1435
+ this.#branchName = newBranchName;
1436
+ this.#pendingMessages = [];
1437
+ }
1438
+ const chain = await this.#store.getMessageChain(messageId);
1439
+ return {
1440
+ id: newBranch.id,
1441
+ name: newBranch.name,
1442
+ headMessageId: newBranch.headMessageId,
1443
+ isActive: switchTo,
1444
+ messageCount: chain.length,
1445
+ createdAt: newBranch.createdAt
1446
+ };
1447
+ }
1363
1448
  /**
1364
1449
  * Get the current chat ID.
1365
1450
  */
@@ -1399,7 +1484,7 @@ var ContextEngine = class {
1399
1484
  if (isMessageFragment(fragment2)) {
1400
1485
  this.#pendingMessages.push(fragment2);
1401
1486
  } else {
1402
- this.#contextFragments.push(fragment2);
1487
+ this.#fragments.push(fragment2);
1403
1488
  }
1404
1489
  }
1405
1490
  return this;
@@ -1409,7 +1494,7 @@ var ContextEngine = class {
1409
1494
  * @internal Use resolve() instead for public API.
1410
1495
  */
1411
1496
  render(renderer) {
1412
- return renderer.render(this.#contextFragments);
1497
+ return renderer.render(this.#fragments);
1413
1498
  }
1414
1499
  /**
1415
1500
  * Resolve context into AI SDK-ready format.
@@ -1428,26 +1513,21 @@ var ContextEngine = class {
1428
1513
  * await generateText({ system: systemPrompt, messages });
1429
1514
  * ```
1430
1515
  */
1431
- async resolve(options = {}) {
1516
+ async resolve(options) {
1432
1517
  await this.#ensureInitialized();
1433
- const renderer = options.renderer ?? new XmlRenderer();
1434
- const systemPrompt = renderer.render(this.#contextFragments);
1435
- const persistedMessages = [];
1518
+ const systemPrompt = options.renderer.render(this.#fragments);
1519
+ const messages = [];
1436
1520
  if (this.#branch?.headMessageId) {
1437
1521
  const chain = await this.#store.getMessageChain(
1438
1522
  this.#branch.headMessageId
1439
1523
  );
1440
- persistedMessages.push(...chain);
1524
+ for (const msg of chain) {
1525
+ messages.push(message(msg.data).codec?.decode());
1526
+ }
1441
1527
  }
1442
- const messages = persistedMessages.map((msg) => ({
1443
- role: msg.name,
1444
- content: String(msg.data)
1445
- }));
1446
1528
  for (const fragment2 of this.#pendingMessages) {
1447
- messages.push({
1448
- role: fragment2.name,
1449
- content: String(fragment2.data)
1450
- });
1529
+ const decoded = fragment2.codec.decode();
1530
+ messages.push(decoded);
1451
1531
  }
1452
1532
  return { systemPrompt, messages };
1453
1533
  }
@@ -1479,8 +1559,7 @@ var ContextEngine = class {
1479
1559
  parentId,
1480
1560
  name: fragment2.name,
1481
1561
  type: fragment2.type,
1482
- data: fragment2.data,
1483
- persist: fragment2.persist ?? true,
1562
+ data: fragment2.codec.encode(),
1484
1563
  createdAt: now
1485
1564
  };
1486
1565
  await this.#store.addMessage(messageData);
@@ -1488,25 +1567,85 @@ var ContextEngine = class {
1488
1567
  }
1489
1568
  await this.#store.updateBranchHead(this.#branch.id, parentId);
1490
1569
  this.#branch.headMessageId = parentId;
1491
- await this.#store.updateChat(this.#chatId, { updatedAt: now });
1492
- if (this.#chatData) {
1493
- this.#chatData.updatedAt = now;
1494
- }
1495
1570
  this.#pendingMessages = [];
1496
1571
  }
1497
1572
  /**
1498
- * Estimate token count and cost for the current context.
1573
+ * Estimate token count and cost for the full context.
1574
+ *
1575
+ * Includes:
1576
+ * - System prompt fragments (role, hints, etc.)
1577
+ * - Persisted chat messages (from store)
1578
+ * - Pending messages (not yet saved)
1499
1579
  *
1500
1580
  * @param modelId - Model ID (e.g., "openai:gpt-4o", "anthropic:claude-3-5-sonnet")
1501
1581
  * @param options - Optional settings
1502
- * @returns Estimate result with token counts and costs
1582
+ * @returns Estimate result with token counts, costs, and per-fragment breakdown
1503
1583
  */
1504
1584
  async estimate(modelId, options = {}) {
1585
+ await this.#ensureInitialized();
1505
1586
  const renderer = options.renderer ?? new XmlRenderer();
1506
- const renderedContext = this.render(renderer);
1507
1587
  const registry = getModelsRegistry();
1508
1588
  await registry.load();
1509
- return registry.estimate(modelId, renderedContext);
1589
+ const model = registry.get(modelId);
1590
+ if (!model) {
1591
+ throw new Error(
1592
+ `Model "${modelId}" not found. Call load() first or check model ID.`
1593
+ );
1594
+ }
1595
+ const tokenizer = registry.getTokenizer(modelId);
1596
+ const fragmentEstimates = [];
1597
+ for (const fragment2 of this.#fragments) {
1598
+ const rendered = renderer.render([fragment2]);
1599
+ const tokens = tokenizer.count(rendered);
1600
+ const cost = tokens / 1e6 * model.cost.input;
1601
+ fragmentEstimates.push({
1602
+ id: fragment2.id,
1603
+ name: fragment2.name,
1604
+ tokens,
1605
+ cost
1606
+ });
1607
+ }
1608
+ if (this.#branch?.headMessageId) {
1609
+ const chain = await this.#store.getMessageChain(
1610
+ this.#branch.headMessageId
1611
+ );
1612
+ for (const msg of chain) {
1613
+ const content = String(msg.data);
1614
+ const tokens = tokenizer.count(content);
1615
+ const cost = tokens / 1e6 * model.cost.input;
1616
+ fragmentEstimates.push({
1617
+ name: msg.name,
1618
+ id: msg.id,
1619
+ tokens,
1620
+ cost
1621
+ });
1622
+ }
1623
+ }
1624
+ for (const fragment2 of this.#pendingMessages) {
1625
+ const content = String(fragment2.data);
1626
+ const tokens = tokenizer.count(content);
1627
+ const cost = tokens / 1e6 * model.cost.input;
1628
+ fragmentEstimates.push({
1629
+ name: fragment2.name,
1630
+ id: fragment2.id,
1631
+ tokens,
1632
+ cost
1633
+ });
1634
+ }
1635
+ const totalTokens = fragmentEstimates.reduce((sum, f) => sum + f.tokens, 0);
1636
+ const totalCost = fragmentEstimates.reduce((sum, f) => sum + f.cost, 0);
1637
+ return {
1638
+ model: model.id,
1639
+ provider: model.provider,
1640
+ tokens: totalTokens,
1641
+ cost: totalCost,
1642
+ limits: {
1643
+ context: model.limit.context,
1644
+ output: model.limit.output,
1645
+ exceedsContext: totalTokens > model.limit.context
1646
+ },
1647
+ fragments: fragmentEstimates
1648
+ };
1510
1649
  }
1511
1650
  /**
1512
1651
  * Rewind to a specific message by ID.
@@ -1533,37 +1672,14 @@ var ContextEngine = class {
1533
1672
  */
1534
1673
  async rewind(messageId) {
1535
1674
  await this.#ensureInitialized();
1536
- const message = await this.#store.getMessage(messageId);
1537
- if (!message) {
1675
+ const message2 = await this.#store.getMessage(messageId);
1676
+ if (!message2) {
1538
1677
  throw new Error(`Message "${messageId}" not found`);
1539
1678
  }
1540
- if (message.chatId !== this.#chatId) {
1679
+ if (message2.chatId !== this.#chatId) {
1541
1680
  throw new Error(`Message "${messageId}" belongs to a different chat`);
1542
1681
  }
1543
- const branches = await this.#store.listBranches(this.#chatId);
1544
- const newBranchName = `${this.#branchName}-v${branches.length + 1}`;
1545
- const newBranch = {
1546
- id: crypto.randomUUID(),
1547
- chatId: this.#chatId,
1548
- name: newBranchName,
1549
- headMessageId: messageId,
1550
- isActive: false,
1551
- createdAt: Date.now()
1552
- };
1553
- await this.#store.createBranch(newBranch);
1554
- await this.#store.setActiveBranch(this.#chatId, newBranch.id);
1555
- this.#branch = { ...newBranch, isActive: true };
1556
- this.#branchName = newBranchName;
1557
- this.#pendingMessages = [];
1558
- const chain = await this.#store.getMessageChain(messageId);
1559
- return {
1560
- id: newBranch.id,
1561
- name: newBranch.name,
1562
- headMessageId: newBranch.headMessageId,
1563
- isActive: true,
1564
- messageCount: chain.length,
1565
- createdAt: newBranch.createdAt
1566
- };
1682
+ return this.#createBranchFrom(messageId, true);
1567
1683
  }
1568
1684
  /**
1569
1685
  * Create a checkpoint at the current position.
@@ -1656,6 +1772,44 @@ var ContextEngine = class {
1656
1772
  this.#branchName = name;
1657
1773
  this.#pendingMessages = [];
1658
1774
  }
1775
+ /**
1776
+ * Create a parallel branch from the current position ("by the way").
1777
+ *
1778
+ * Use this when you want to fork the conversation without leaving
1779
+ * the current branch. Common use case: user wants to ask another
1780
+ * question while waiting for the model to respond.
1781
+ *
1782
+ * Unlike rewind(), this method:
1783
+ * - Uses the current HEAD (no messageId needed)
1784
+ * - Does NOT switch to the new branch
1785
+ * - Keeps pending messages intact
1786
+ *
1787
+ * @returns The new branch info (does not switch to it)
1788
+ * @throws Error if no messages exist in the conversation
1789
+ *
1790
+ * @example
1791
+ * ```ts
1792
+ * // User asked a question, model is generating...
1793
+ * context.set(user('What is the weather?'));
1794
+ * await context.save();
1795
+ *
1796
+ * // User wants to ask something else without waiting
1797
+ * const newBranch = await context.btw();
1798
+ * // newBranch = { name: 'main-v2', ... }
1799
+ *
1800
+ * // Later, switch to the new branch and add the question
1801
+ * await context.switchBranch(newBranch.name);
1802
+ * context.set(user('Also, what time is it?'));
1803
+ * await context.save();
1804
+ * ```
1805
+ */
1806
+ async btw() {
1807
+ await this.#ensureInitialized();
1808
+ if (!this.#branch?.headMessageId) {
1809
+ throw new Error("Cannot create btw branch: no messages in conversation");
1810
+ }
1811
+ return this.#createBranchFrom(this.#branch.headMessageId, false);
1812
+ }
1659
1813
  /**
1660
1814
  * Update metadata for the current chat.
1661
1815
  *
@@ -1671,10 +1825,7 @@ var ContextEngine = class {
1671
1825
  */
1672
1826
  async updateChat(updates) {
1673
1827
  await this.#ensureInitialized();
1674
- const now = Date.now();
1675
- const storeUpdates = {
1676
- updatedAt: now
1677
- };
1828
+ const storeUpdates = {};
1678
1829
  if (updates.title !== void 0) {
1679
1830
  storeUpdates.title = updates.title;
1680
1831
  }
@@ -1684,16 +1835,7 @@ var ContextEngine = class {
1684
1835
  ...updates.metadata
1685
1836
  };
1686
1837
  }
1687
- await this.#store.updateChat(this.#chatId, storeUpdates);
1688
- if (this.#chatData) {
1689
- if (storeUpdates.title !== void 0) {
1690
- this.#chatData.title = storeUpdates.title;
1691
- }
1692
- if (storeUpdates.metadata !== void 0) {
1693
- this.#chatData.metadata = storeUpdates.metadata;
1694
- }
1695
- this.#chatData.updatedAt = now;
1696
- }
1838
+ this.#chatData = await this.#store.updateChat(this.#chatId, storeUpdates);
1697
1839
  }
1698
1840
  /**
1699
1841
  * Consolidate context fragments (no-op for now).
@@ -1706,6 +1848,54 @@ var ContextEngine = class {
1706
1848
  consolidate() {
1707
1849
  return void 0;
1708
1850
  }
1851
+ /**
1852
+ * Inspect the full context state for debugging.
1853
+ * Returns a comprehensive JSON-serializable object with all context information.
1854
+ *
1855
+ * @param options - Inspection options (modelId and renderer required)
1856
+ * @returns Complete inspection data including estimates, rendered output, fragments, and graph
1857
+ *
1858
+ * @example
1859
+ * ```ts
1860
+ * const inspection = await context.inspect({
1861
+ * modelId: 'openai:gpt-4o',
1862
+ * renderer: new XmlRenderer(),
1863
+ * });
1864
+ * console.log(JSON.stringify(inspection, null, 2));
1865
+ *
1866
+ * // Or write to file for analysis
1867
+ * await fs.writeFile('context-debug.json', JSON.stringify(inspection, null, 2));
1868
+ * ```
1869
+ */
1870
+ async inspect(options) {
1871
+ await this.#ensureInitialized();
1872
+ const { renderer } = options;
1873
+ const estimateResult = await this.estimate(options.modelId, { renderer });
1874
+ const rendered = renderer.render(this.#fragments);
1875
+ const persistedMessages = [];
1876
+ if (this.#branch?.headMessageId) {
1877
+ const chain = await this.#store.getMessageChain(
1878
+ this.#branch.headMessageId
1879
+ );
1880
+ persistedMessages.push(...chain);
1881
+ }
1882
+ const graph = await this.#store.getGraph(this.#chatId);
1883
+ return {
1884
+ estimate: estimateResult,
1885
+ rendered,
1886
+ fragments: {
1887
+ context: [...this.#fragments],
1888
+ pending: [...this.#pendingMessages],
1889
+ persisted: persistedMessages
1890
+ },
1891
+ graph,
1892
+ meta: {
1893
+ chatId: this.#chatId,
1894
+ branch: this.#branchName,
1895
+ timestamp: Date.now()
1896
+ }
1897
+ };
1898
+ }
1709
1899
  };
1710
1900
  function hint(text) {
1711
1901
  return {
@@ -1725,24 +1915,75 @@ function role(content) {
1725
1915
  data: content
1726
1916
  };
1727
1917
  }
1728
- function user(content, options) {
1918
+ function user(content) {
1919
+ const message2 = typeof content === "string" ? {
1920
+ id: generateId(),
1921
+ role: "user",
1922
+ parts: [{ type: "text", text: content }]
1923
+ } : content;
1729
1924
  return {
1730
- id: options?.id ?? crypto.randomUUID(),
1925
+ id: message2.id,
1731
1926
  name: "user",
1732
- data: content,
1927
+ data: "content",
1733
1928
  type: "message",
1734
- persist: true
1929
+ persist: true,
1930
+ codec: {
1931
+ decode() {
1932
+ return message2;
1933
+ },
1934
+ encode() {
1935
+ return message2;
1936
+ }
1937
+ }
1735
1938
  };
1736
1939
  }
1737
- function assistant(content, options) {
1940
+ function assistant(message2) {
1738
1941
  return {
1739
- id: options?.id ?? crypto.randomUUID(),
1942
+ id: message2.id,
1740
1943
  name: "assistant",
1741
- data: content,
1944
+ data: "content",
1742
1945
  type: "message",
1743
- persist: true
1946
+ persist: true,
1947
+ codec: {
1948
+ decode() {
1949
+ return message2;
1950
+ },
1951
+ encode() {
1952
+ return message2;
1953
+ }
1954
+ }
1744
1955
  };
1745
1956
  }
1957
+ function message(content) {
1958
+ const message2 = typeof content === "string" ? {
1959
+ id: generateId(),
1960
+ role: "user",
1961
+ parts: [{ type: "text", text: content }]
1962
+ } : content;
1963
+ return {
1964
+ id: message2.id,
1965
+ name: "message",
1966
+ data: "content",
1967
+ type: "message",
1968
+ persist: true,
1969
+ codec: {
1970
+ decode() {
1971
+ return message2;
1972
+ },
1973
+ encode() {
1974
+ return message2;
1975
+ }
1976
+ }
1977
+ };
1978
+ }
1979
+ function assistantText(content, options) {
1980
+ const id = options?.id ?? crypto.randomUUID();
1981
+ return assistant({
1982
+ id,
1983
+ role: "assistant",
1984
+ parts: [{ type: "text", text: content }]
1985
+ });
1986
+ }
1746
1987
  export {
1747
1988
  ContextEngine,
1748
1989
  ContextStore,
@@ -1754,11 +1995,14 @@ export {
1754
1995
  ToonRenderer,
1755
1996
  XmlRenderer,
1756
1997
  assistant,
1998
+ assistantText,
1757
1999
  defaultTokenizer,
2000
+ estimate,
1758
2001
  fragment,
1759
2002
  getModelsRegistry,
1760
2003
  hint,
1761
2004
  isMessageFragment,
2005
+ message,
1762
2006
  role,
1763
2007
  user,
1764
2008
  visualizeGraph