@deepagents/context 0.10.2 → 0.11.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.
Files changed (42) hide show
  1. package/README.md +114 -119
  2. package/dist/example-error-recovery.d.ts +2 -0
  3. package/dist/example-error-recovery.d.ts.map +1 -0
  4. package/dist/index.d.ts +16 -388
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +2391 -1062
  7. package/dist/index.js.map +4 -4
  8. package/dist/lib/agent.d.ts +86 -12
  9. package/dist/lib/agent.d.ts.map +1 -1
  10. package/dist/lib/engine.d.ts +323 -0
  11. package/dist/lib/engine.d.ts.map +1 -0
  12. package/dist/lib/estimate.d.ts +1 -1
  13. package/dist/lib/estimate.d.ts.map +1 -1
  14. package/dist/lib/fragments/domain.d.ts +440 -0
  15. package/dist/lib/fragments/domain.d.ts.map +1 -0
  16. package/dist/lib/fragments/user.d.ts +122 -0
  17. package/dist/lib/fragments/user.d.ts.map +1 -0
  18. package/dist/lib/fragments.d.ts +107 -0
  19. package/dist/lib/fragments.d.ts.map +1 -0
  20. package/dist/lib/guardrail.d.ts +138 -0
  21. package/dist/lib/guardrail.d.ts.map +1 -0
  22. package/dist/lib/renderers/abstract.renderer.d.ts +1 -1
  23. package/dist/lib/renderers/abstract.renderer.d.ts.map +1 -1
  24. package/dist/lib/sandbox/binary-bridges.d.ts +31 -0
  25. package/dist/lib/sandbox/binary-bridges.d.ts.map +1 -0
  26. package/dist/lib/sandbox/container-tool.d.ts +134 -0
  27. package/dist/lib/sandbox/container-tool.d.ts.map +1 -0
  28. package/dist/lib/sandbox/docker-sandbox.d.ts +471 -0
  29. package/dist/lib/sandbox/docker-sandbox.d.ts.map +1 -0
  30. package/dist/lib/sandbox/index.d.ts +4 -0
  31. package/dist/lib/sandbox/index.d.ts.map +1 -0
  32. package/dist/lib/skills/fragments.d.ts +24 -0
  33. package/dist/lib/skills/fragments.d.ts.map +1 -0
  34. package/dist/lib/skills/index.d.ts +31 -0
  35. package/dist/lib/skills/index.d.ts.map +1 -0
  36. package/dist/lib/skills/loader.d.ts +28 -0
  37. package/dist/lib/skills/loader.d.ts.map +1 -0
  38. package/dist/lib/skills/types.d.ts +40 -0
  39. package/dist/lib/skills/types.d.ts.map +1 -0
  40. package/package.json +7 -3
  41. package/dist/lib/context.d.ts +0 -56
  42. package/dist/lib/context.d.ts.map +0 -1
package/dist/index.js CHANGED
@@ -1,17 +1,3 @@
1
- // packages/context/src/index.ts
2
- import { generateId } from "ai";
3
-
4
- // packages/context/src/lib/context.ts
5
- function isFragment(data) {
6
- return typeof data === "object" && data !== null && "name" in data && "data" in data && typeof data.name === "string";
7
- }
8
- function isFragmentObject(data) {
9
- return typeof data === "object" && data !== null && !Array.isArray(data) && !isFragment(data);
10
- }
11
- function isMessageFragment(fragment2) {
12
- return fragment2.type === "message";
13
- }
14
-
15
1
  // packages/context/src/lib/estimate.ts
16
2
  import { encode } from "gpt-tokenizer";
17
3
  var defaultTokenizer = {
@@ -172,6 +158,99 @@ async function estimate(modelId, renderer, ...fragments) {
172
158
  };
173
159
  }
174
160
 
161
+ // packages/context/src/lib/fragments.ts
162
+ import { generateId } from "ai";
163
+ function isFragment(data) {
164
+ return typeof data === "object" && data !== null && "name" in data && "data" in data && typeof data.name === "string";
165
+ }
166
+ function isFragmentObject(data) {
167
+ return typeof data === "object" && data !== null && !Array.isArray(data) && !isFragment(data);
168
+ }
169
+ function isMessageFragment(fragment2) {
170
+ return fragment2.type === "message";
171
+ }
172
+ function fragment(name, ...children) {
173
+ return {
174
+ name,
175
+ data: children
176
+ };
177
+ }
178
+ function role(content) {
179
+ return {
180
+ name: "role",
181
+ data: content
182
+ };
183
+ }
184
+ function user(content) {
185
+ const message2 = typeof content === "string" ? {
186
+ id: generateId(),
187
+ role: "user",
188
+ parts: [{ type: "text", text: content }]
189
+ } : content;
190
+ return {
191
+ id: message2.id,
192
+ name: "user",
193
+ data: "content",
194
+ type: "message",
195
+ persist: true,
196
+ codec: {
197
+ decode() {
198
+ return message2;
199
+ },
200
+ encode() {
201
+ return message2;
202
+ }
203
+ }
204
+ };
205
+ }
206
+ function assistant(message2) {
207
+ return {
208
+ id: message2.id,
209
+ name: "assistant",
210
+ data: "content",
211
+ type: "message",
212
+ persist: true,
213
+ codec: {
214
+ decode() {
215
+ return message2;
216
+ },
217
+ encode() {
218
+ return message2;
219
+ }
220
+ }
221
+ };
222
+ }
223
+ function message(content) {
224
+ const message2 = typeof content === "string" ? {
225
+ id: generateId(),
226
+ role: "user",
227
+ parts: [{ type: "text", text: content }]
228
+ } : content;
229
+ return {
230
+ id: message2.id,
231
+ name: "message",
232
+ data: "content",
233
+ type: "message",
234
+ persist: true,
235
+ codec: {
236
+ decode() {
237
+ return message2;
238
+ },
239
+ encode() {
240
+ return message2;
241
+ }
242
+ }
243
+ };
244
+ }
245
+ function assistantText(content, options) {
246
+ const id = options?.id ?? crypto.randomUUID();
247
+ return assistant({
248
+ id,
249
+ role: "assistant",
250
+ parts: [{ type: "text", text: content }]
251
+ });
252
+ }
253
+
175
254
  // packages/context/src/lib/renderers/abstract.renderer.ts
176
255
  import pluralize from "pluralize";
177
256
  import { titlecase } from "stringcase";
@@ -547,13 +626,13 @@ var TomlRenderer = class extends ContextRenderer {
547
626
  const entries = this.#renderObjectEntries(obj, newPath);
548
627
  return ["", `[${newPath.join(".")}]`, ...entries].join("\n");
549
628
  }
550
- #renderObjectEntries(obj, path) {
629
+ #renderObjectEntries(obj, path3) {
551
630
  return Object.entries(obj).map(([key, value]) => {
552
631
  if (value == null) {
553
632
  return "";
554
633
  }
555
634
  if (isFragmentObject(value)) {
556
- const newPath = [...path, key];
635
+ const newPath = [...path3, key];
557
636
  const entries = this.#renderObjectEntries(value, newPath);
558
637
  return ["", `[${newPath.join(".")}]`, ...entries].join("\n");
559
638
  }
@@ -826,1185 +905,2435 @@ ${entries}`;
826
905
  var ContextStore = class {
827
906
  };
828
907
 
829
- // packages/context/src/lib/store/sqlite.store.ts
830
- import { DatabaseSync } from "node:sqlite";
831
- var STORE_DDL = `
832
- -- Chats table
833
- -- createdAt/updatedAt: DEFAULT for insert, inline SET for updates
834
- CREATE TABLE IF NOT EXISTS chats (
835
- id TEXT PRIMARY KEY,
836
- title TEXT,
837
- metadata TEXT,
838
- createdAt INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
839
- updatedAt INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)
840
- );
841
-
842
- CREATE INDEX IF NOT EXISTS idx_chats_updatedAt ON chats(updatedAt);
843
-
844
- -- Messages table (nodes in the DAG)
845
- CREATE TABLE IF NOT EXISTS messages (
846
- id TEXT PRIMARY KEY,
847
- chatId TEXT NOT NULL,
848
- parentId TEXT,
849
- name TEXT NOT NULL,
850
- type TEXT,
851
- data TEXT NOT NULL,
852
- createdAt INTEGER NOT NULL,
853
- FOREIGN KEY (chatId) REFERENCES chats(id) ON DELETE CASCADE,
854
- FOREIGN KEY (parentId) REFERENCES messages(id)
855
- );
856
-
857
- CREATE INDEX IF NOT EXISTS idx_messages_chatId ON messages(chatId);
858
- CREATE INDEX IF NOT EXISTS idx_messages_parentId ON messages(parentId);
859
-
860
- -- Branches table (pointers to head messages)
861
- CREATE TABLE IF NOT EXISTS branches (
862
- id TEXT PRIMARY KEY,
863
- chatId TEXT NOT NULL,
864
- name TEXT NOT NULL,
865
- headMessageId TEXT,
866
- isActive INTEGER NOT NULL DEFAULT 0,
867
- createdAt INTEGER NOT NULL,
868
- FOREIGN KEY (chatId) REFERENCES chats(id) ON DELETE CASCADE,
869
- FOREIGN KEY (headMessageId) REFERENCES messages(id),
870
- UNIQUE(chatId, name)
871
- );
872
-
873
- CREATE INDEX IF NOT EXISTS idx_branches_chatId ON branches(chatId);
874
-
875
- -- Checkpoints table (pointers to message nodes)
876
- CREATE TABLE IF NOT EXISTS checkpoints (
877
- id TEXT PRIMARY KEY,
878
- chatId TEXT NOT NULL,
879
- name TEXT NOT NULL,
880
- messageId TEXT NOT NULL,
881
- createdAt INTEGER NOT NULL,
882
- FOREIGN KEY (chatId) REFERENCES chats(id) ON DELETE CASCADE,
883
- FOREIGN KEY (messageId) REFERENCES messages(id),
884
- UNIQUE(chatId, name)
885
- );
886
-
887
- CREATE INDEX IF NOT EXISTS idx_checkpoints_chatId ON checkpoints(chatId);
888
-
889
- -- FTS5 virtual table for full-text search
890
- -- messageId/chatId/name are UNINDEXED (stored but not searchable, used for filtering/joining)
891
- -- Only 'content' is indexed for full-text search
892
- CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
893
- messageId UNINDEXED,
894
- chatId UNINDEXED,
895
- name UNINDEXED,
896
- content,
897
- tokenize='porter unicode61'
898
- );
899
- `;
900
- var SqliteContextStore = class extends ContextStore {
901
- #db;
902
- constructor(path) {
903
- super();
904
- this.#db = new DatabaseSync(path);
905
- this.#db.exec("PRAGMA foreign_keys = ON");
906
- this.#db.exec(STORE_DDL);
908
+ // packages/context/src/lib/engine.ts
909
+ var ContextEngine = class {
910
+ /** Non-message fragments (role, hints, etc.) - not persisted in graph */
911
+ #fragments = [];
912
+ /** Pending message fragments to be added to graph */
913
+ #pendingMessages = [];
914
+ #store;
915
+ #chatId;
916
+ #branchName;
917
+ #branch = null;
918
+ #chatData = null;
919
+ #initialized = false;
920
+ constructor(options) {
921
+ if (!options.chatId) {
922
+ throw new Error("chatId is required");
923
+ }
924
+ this.#store = options.store;
925
+ this.#chatId = options.chatId;
926
+ this.#branchName = options.branch ?? "main";
907
927
  }
908
- // ==========================================================================
909
- // Chat Operations
910
- // ==========================================================================
911
- async createChat(chat) {
912
- this.#db.prepare(
913
- `INSERT INTO chats (id, title, metadata)
914
- VALUES (?, ?, ?)`
915
- ).run(
916
- chat.id,
917
- chat.title ?? null,
918
- chat.metadata ? JSON.stringify(chat.metadata) : null
928
+ /**
929
+ * Initialize the chat and branch if they don't exist.
930
+ */
931
+ async #ensureInitialized() {
932
+ if (this.#initialized) {
933
+ return;
934
+ }
935
+ this.#chatData = await this.#store.upsertChat({ id: this.#chatId });
936
+ const existingBranch = await this.#store.getBranch(
937
+ this.#chatId,
938
+ this.#branchName
919
939
  );
940
+ if (existingBranch) {
941
+ this.#branch = existingBranch;
942
+ } else {
943
+ this.#branch = {
944
+ id: crypto.randomUUID(),
945
+ chatId: this.#chatId,
946
+ name: this.#branchName,
947
+ headMessageId: null,
948
+ isActive: true,
949
+ createdAt: Date.now()
950
+ };
951
+ await this.#store.createBranch(this.#branch);
952
+ }
953
+ this.#initialized = true;
920
954
  }
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
955
+ /**
956
+ * Create a new branch from a specific message.
957
+ * Shared logic between rewind() and btw().
958
+ */
959
+ async #createBranchFrom(messageId, switchTo) {
960
+ const branches = await this.#store.listBranches(this.#chatId);
961
+ const samePrefix = branches.filter(
962
+ (b) => b.name === this.#branchName || b.name.startsWith(`${this.#branchName}-v`)
931
963
  );
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
- }
940
- async getChat(chatId) {
941
- const row = this.#db.prepare("SELECT * FROM chats WHERE id = ?").get(chatId);
942
- if (!row) {
943
- return void 0;
944
- }
945
- return {
946
- id: row.id,
947
- title: row.title ?? void 0,
948
- metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
949
- createdAt: row.createdAt,
950
- updatedAt: row.updatedAt
964
+ const newBranchName = `${this.#branchName}-v${samePrefix.length + 1}`;
965
+ const newBranch = {
966
+ id: crypto.randomUUID(),
967
+ chatId: this.#chatId,
968
+ name: newBranchName,
969
+ headMessageId: messageId,
970
+ isActive: false,
971
+ createdAt: Date.now()
951
972
  };
952
- }
953
- async updateChat(chatId, updates) {
954
- const setClauses = ["updatedAt = strftime('%s', 'now') * 1000"];
955
- const params = [];
956
- if (updates.title !== void 0) {
957
- setClauses.push("title = ?");
958
- params.push(updates.title ?? null);
959
- }
960
- if (updates.metadata !== void 0) {
961
- setClauses.push("metadata = ?");
962
- params.push(JSON.stringify(updates.metadata));
973
+ await this.#store.createBranch(newBranch);
974
+ if (switchTo) {
975
+ await this.#store.setActiveBranch(this.#chatId, newBranch.id);
976
+ this.#branch = { ...newBranch, isActive: true };
977
+ this.#branchName = newBranchName;
978
+ this.#pendingMessages = [];
963
979
  }
964
- params.push(chatId);
965
- const row = this.#db.prepare(
966
- `UPDATE chats SET ${setClauses.join(", ")} WHERE id = ? RETURNING *`
967
- ).get(...params);
980
+ const chain = await this.#store.getMessageChain(messageId);
968
981
  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
982
+ id: newBranch.id,
983
+ name: newBranch.name,
984
+ headMessageId: newBranch.headMessageId,
985
+ isActive: switchTo,
986
+ messageCount: chain.length,
987
+ createdAt: newBranch.createdAt
974
988
  };
975
989
  }
976
- async listChats() {
977
- const rows = this.#db.prepare(
978
- `SELECT
979
- c.id,
980
- c.title,
981
- c.createdAt,
982
- c.updatedAt,
983
- COUNT(DISTINCT m.id) as messageCount,
984
- COUNT(DISTINCT b.id) as branchCount
985
- FROM chats c
986
- LEFT JOIN messages m ON m.chatId = c.id
987
- LEFT JOIN branches b ON b.chatId = c.id
988
- GROUP BY c.id
989
- ORDER BY c.updatedAt DESC`
990
- ).all();
991
- return rows.map((row) => ({
992
- id: row.id,
993
- title: row.title ?? void 0,
994
- messageCount: row.messageCount,
995
- branchCount: row.branchCount,
996
- createdAt: row.createdAt,
997
- updatedAt: row.updatedAt
998
- }));
990
+ /**
991
+ * Get the current chat ID.
992
+ */
993
+ get chatId() {
994
+ return this.#chatId;
999
995
  }
1000
- // ==========================================================================
1001
- // Message Operations (Graph Nodes)
1002
- // ==========================================================================
1003
- async addMessage(message2) {
1004
- this.#db.prepare(
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`
1012
- ).run(
1013
- message2.id,
1014
- message2.chatId,
1015
- message2.parentId,
1016
- message2.name,
1017
- message2.type ?? null,
1018
- JSON.stringify(message2.data),
1019
- message2.createdAt
1020
- );
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);
1023
- this.#db.prepare(
1024
- `INSERT INTO messages_fts(messageId, chatId, name, content)
1025
- VALUES (?, ?, ?, ?)`
1026
- ).run(message2.id, message2.chatId, message2.name, content);
996
+ /**
997
+ * Get the current branch name.
998
+ */
999
+ get branch() {
1000
+ return this.#branchName;
1027
1001
  }
1028
- async getMessage(messageId) {
1029
- const row = this.#db.prepare("SELECT * FROM messages WHERE id = ?").get(messageId);
1030
- if (!row) {
1031
- return void 0;
1002
+ /**
1003
+ * Get metadata for the current chat.
1004
+ * Returns null if the chat hasn't been initialized yet.
1005
+ */
1006
+ get chat() {
1007
+ if (!this.#chatData) {
1008
+ return null;
1032
1009
  }
1033
1010
  return {
1034
- id: row.id,
1035
- chatId: row.chatId,
1036
- parentId: row.parentId,
1037
- name: row.name,
1038
- type: row.type ?? void 0,
1039
- data: JSON.parse(row.data),
1040
- createdAt: row.createdAt
1011
+ id: this.#chatData.id,
1012
+ createdAt: this.#chatData.createdAt,
1013
+ updatedAt: this.#chatData.updatedAt,
1014
+ title: this.#chatData.title,
1015
+ metadata: this.#chatData.metadata
1041
1016
  };
1042
1017
  }
1043
- async getMessageChain(headId) {
1044
- const rows = this.#db.prepare(
1045
- `WITH RECURSIVE chain AS (
1046
- SELECT *, 0 as depth FROM messages WHERE id = ?
1047
- UNION ALL
1048
- SELECT m.*, c.depth + 1 FROM messages m
1049
- INNER JOIN chain c ON m.id = c.parentId
1050
- )
1051
- SELECT * FROM chain
1052
- ORDER BY depth DESC`
1053
- ).all(headId);
1054
- return rows.map((row) => ({
1055
- id: row.id,
1056
- chatId: row.chatId,
1057
- parentId: row.parentId,
1058
- name: row.name,
1059
- type: row.type ?? void 0,
1060
- data: JSON.parse(row.data),
1061
- createdAt: row.createdAt
1062
- }));
1018
+ /**
1019
+ * Add fragments to the context.
1020
+ *
1021
+ * - Message fragments (user/assistant) are queued for persistence
1022
+ * - Non-message fragments (role/hint) are kept in memory for system prompt
1023
+ */
1024
+ set(...fragments) {
1025
+ for (const fragment2 of fragments) {
1026
+ if (isMessageFragment(fragment2)) {
1027
+ this.#pendingMessages.push(fragment2);
1028
+ } else {
1029
+ this.#fragments.push(fragment2);
1030
+ }
1031
+ }
1032
+ return this;
1063
1033
  }
1064
- async hasChildren(messageId) {
1065
- const row = this.#db.prepare(
1066
- "SELECT EXISTS(SELECT 1 FROM messages WHERE parentId = ?) as hasChildren"
1067
- ).get(messageId);
1068
- return row.hasChildren === 1;
1034
+ // Unset a fragment by ID (not implemented yet)
1035
+ unset(fragmentId) {
1069
1036
  }
1070
- // ==========================================================================
1071
- // Branch Operations
1072
- // ==========================================================================
1073
- async createBranch(branch) {
1074
- this.#db.prepare(
1075
- `INSERT INTO branches (id, chatId, name, headMessageId, isActive, createdAt)
1076
- VALUES (?, ?, ?, ?, ?, ?)`
1077
- ).run(
1078
- branch.id,
1079
- branch.chatId,
1080
- branch.name,
1081
- branch.headMessageId,
1082
- branch.isActive ? 1 : 0,
1083
- branch.createdAt
1084
- );
1037
+ /**
1038
+ * Render all fragments using the provided renderer.
1039
+ * @internal Use resolve() instead for public API.
1040
+ */
1041
+ render(renderer) {
1042
+ return renderer.render(this.#fragments);
1085
1043
  }
1086
- async getBranch(chatId, name) {
1087
- const row = this.#db.prepare("SELECT * FROM branches WHERE chatId = ? AND name = ?").get(chatId, name);
1088
- if (!row) {
1089
- return void 0;
1044
+ /**
1045
+ * Resolve context into AI SDK-ready format.
1046
+ *
1047
+ * - Initializes chat and branch if needed
1048
+ * - Loads message history from the graph (walking parent chain)
1049
+ * - Separates context fragments for system prompt
1050
+ * - Combines with pending messages
1051
+ *
1052
+ * @example
1053
+ * ```ts
1054
+ * const context = new ContextEngine({ store, chatId: 'chat-1' })
1055
+ * .set(role('You are helpful'), user('Hello'));
1056
+ *
1057
+ * const { systemPrompt, messages } = await context.resolve();
1058
+ * await generateText({ system: systemPrompt, messages });
1059
+ * ```
1060
+ */
1061
+ async resolve(options) {
1062
+ await this.#ensureInitialized();
1063
+ const systemPrompt = options.renderer.render(this.#fragments);
1064
+ const messages = [];
1065
+ if (this.#branch?.headMessageId) {
1066
+ const chain = await this.#store.getMessageChain(
1067
+ this.#branch.headMessageId
1068
+ );
1069
+ for (const msg of chain) {
1070
+ messages.push(message(msg.data).codec?.decode());
1071
+ }
1090
1072
  }
1091
- return {
1092
- id: row.id,
1093
- chatId: row.chatId,
1094
- name: row.name,
1095
- headMessageId: row.headMessageId,
1096
- isActive: row.isActive === 1,
1097
- createdAt: row.createdAt
1098
- };
1099
- }
1100
- async getActiveBranch(chatId) {
1101
- const row = this.#db.prepare("SELECT * FROM branches WHERE chatId = ? AND isActive = 1").get(chatId);
1102
- if (!row) {
1103
- return void 0;
1073
+ for (const fragment2 of this.#pendingMessages) {
1074
+ const decoded = fragment2.codec.decode();
1075
+ messages.push(decoded);
1104
1076
  }
1105
- return {
1106
- id: row.id,
1107
- chatId: row.chatId,
1108
- name: row.name,
1109
- headMessageId: row.headMessageId,
1110
- isActive: true,
1111
- createdAt: row.createdAt
1112
- };
1113
- }
1114
- async setActiveBranch(chatId, branchId) {
1115
- this.#db.prepare("UPDATE branches SET isActive = 0 WHERE chatId = ?").run(chatId);
1116
- this.#db.prepare("UPDATE branches SET isActive = 1 WHERE id = ?").run(branchId);
1077
+ return { systemPrompt, messages };
1117
1078
  }
1118
- async updateBranchHead(branchId, messageId) {
1119
- this.#db.prepare("UPDATE branches SET headMessageId = ? WHERE id = ?").run(messageId, branchId);
1079
+ /**
1080
+ * Save pending messages to the graph.
1081
+ *
1082
+ * Each message is added as a node with parentId pointing to the previous message.
1083
+ * The branch head is updated to point to the last message.
1084
+ *
1085
+ * @example
1086
+ * ```ts
1087
+ * context.set(user('Hello'));
1088
+ * // AI responds...
1089
+ * context.set(assistant('Hi there!'));
1090
+ * await context.save(); // Persist to graph
1091
+ * ```
1092
+ */
1093
+ async save() {
1094
+ await this.#ensureInitialized();
1095
+ if (this.#pendingMessages.length === 0) {
1096
+ return;
1097
+ }
1098
+ let parentId = this.#branch.headMessageId;
1099
+ const now = Date.now();
1100
+ for (const fragment2 of this.#pendingMessages) {
1101
+ const messageData = {
1102
+ id: fragment2.id ?? crypto.randomUUID(),
1103
+ chatId: this.#chatId,
1104
+ parentId,
1105
+ name: fragment2.name,
1106
+ type: fragment2.type,
1107
+ data: fragment2.codec.encode(),
1108
+ createdAt: now
1109
+ };
1110
+ await this.#store.addMessage(messageData);
1111
+ parentId = messageData.id;
1112
+ }
1113
+ await this.#store.updateBranchHead(this.#branch.id, parentId);
1114
+ this.#branch.headMessageId = parentId;
1115
+ this.#pendingMessages = [];
1120
1116
  }
1121
- async listBranches(chatId) {
1122
- const branches = this.#db.prepare(
1123
- `SELECT
1124
- b.id,
1125
- b.name,
1126
- b.headMessageId,
1127
- b.isActive,
1128
- b.createdAt
1129
- FROM branches b
1130
- WHERE b.chatId = ?
1131
- ORDER BY b.createdAt ASC`
1132
- ).all(chatId);
1133
- const result = [];
1134
- for (const branch of branches) {
1135
- let messageCount = 0;
1136
- if (branch.headMessageId) {
1137
- const countRow = this.#db.prepare(
1138
- `WITH RECURSIVE chain AS (
1139
- SELECT id, parentId FROM messages WHERE id = ?
1140
- UNION ALL
1141
- SELECT m.id, m.parentId FROM messages m
1142
- INNER JOIN chain c ON m.id = c.parentId
1143
- )
1144
- SELECT COUNT(*) as count FROM chain`
1145
- ).get(branch.headMessageId);
1146
- messageCount = countRow.count;
1117
+ /**
1118
+ * Estimate token count and cost for the full context.
1119
+ *
1120
+ * Includes:
1121
+ * - System prompt fragments (role, hints, etc.)
1122
+ * - Persisted chat messages (from store)
1123
+ * - Pending messages (not yet saved)
1124
+ *
1125
+ * @param modelId - Model ID (e.g., "openai:gpt-4o", "anthropic:claude-3-5-sonnet")
1126
+ * @param options - Optional settings
1127
+ * @returns Estimate result with token counts, costs, and per-fragment breakdown
1128
+ */
1129
+ async estimate(modelId, options = {}) {
1130
+ await this.#ensureInitialized();
1131
+ const renderer = options.renderer ?? new XmlRenderer();
1132
+ const registry = getModelsRegistry();
1133
+ await registry.load();
1134
+ const model = registry.get(modelId);
1135
+ if (!model) {
1136
+ throw new Error(
1137
+ `Model "${modelId}" not found. Call load() first or check model ID.`
1138
+ );
1139
+ }
1140
+ const tokenizer = registry.getTokenizer(modelId);
1141
+ const fragmentEstimates = [];
1142
+ for (const fragment2 of this.#fragments) {
1143
+ const rendered = renderer.render([fragment2]);
1144
+ const tokens = tokenizer.count(rendered);
1145
+ const cost = tokens / 1e6 * model.cost.input;
1146
+ fragmentEstimates.push({
1147
+ id: fragment2.id,
1148
+ name: fragment2.name,
1149
+ tokens,
1150
+ cost
1151
+ });
1152
+ }
1153
+ if (this.#branch?.headMessageId) {
1154
+ const chain = await this.#store.getMessageChain(
1155
+ this.#branch.headMessageId
1156
+ );
1157
+ for (const msg of chain) {
1158
+ const content = String(msg.data);
1159
+ const tokens = tokenizer.count(content);
1160
+ const cost = tokens / 1e6 * model.cost.input;
1161
+ fragmentEstimates.push({
1162
+ name: msg.name,
1163
+ id: msg.id,
1164
+ tokens,
1165
+ cost
1166
+ });
1147
1167
  }
1148
- result.push({
1149
- id: branch.id,
1150
- name: branch.name,
1151
- headMessageId: branch.headMessageId,
1152
- isActive: branch.isActive === 1,
1153
- messageCount,
1154
- createdAt: branch.createdAt
1168
+ }
1169
+ for (const fragment2 of this.#pendingMessages) {
1170
+ const content = String(fragment2.data);
1171
+ const tokens = tokenizer.count(content);
1172
+ const cost = tokens / 1e6 * model.cost.input;
1173
+ fragmentEstimates.push({
1174
+ name: fragment2.name,
1175
+ id: fragment2.id,
1176
+ tokens,
1177
+ cost
1155
1178
  });
1156
1179
  }
1157
- return result;
1180
+ const totalTokens = fragmentEstimates.reduce((sum, f) => sum + f.tokens, 0);
1181
+ const totalCost = fragmentEstimates.reduce((sum, f) => sum + f.cost, 0);
1182
+ return {
1183
+ model: model.id,
1184
+ provider: model.provider,
1185
+ tokens: totalTokens,
1186
+ cost: totalCost,
1187
+ limits: {
1188
+ context: model.limit.context,
1189
+ output: model.limit.output,
1190
+ exceedsContext: totalTokens > model.limit.context
1191
+ },
1192
+ fragments: fragmentEstimates
1193
+ };
1158
1194
  }
1159
- // ==========================================================================
1160
- // Checkpoint Operations
1161
- // ==========================================================================
1162
- async createCheckpoint(checkpoint) {
1163
- this.#db.prepare(
1164
- `INSERT INTO checkpoints (id, chatId, name, messageId, createdAt)
1165
- VALUES (?, ?, ?, ?, ?)
1166
- ON CONFLICT(chatId, name) DO UPDATE SET
1167
- messageId = excluded.messageId,
1168
- createdAt = excluded.createdAt`
1169
- ).run(
1170
- checkpoint.id,
1171
- checkpoint.chatId,
1172
- checkpoint.name,
1173
- checkpoint.messageId,
1174
- checkpoint.createdAt
1175
- );
1195
+ /**
1196
+ * Rewind to a specific message by ID.
1197
+ *
1198
+ * Creates a new branch from that message, preserving the original branch.
1199
+ * The new branch becomes active.
1200
+ *
1201
+ * @param messageId - The message ID to rewind to
1202
+ * @returns The new branch info
1203
+ *
1204
+ * @example
1205
+ * ```ts
1206
+ * context.set(user('What is 2 + 2?', { id: 'q1' }));
1207
+ * context.set(assistant('The answer is 5.', { id: 'wrong' })); // Oops!
1208
+ * await context.save();
1209
+ *
1210
+ * // Rewind to the question, creates new branch
1211
+ * const newBranch = await context.rewind('q1');
1212
+ *
1213
+ * // Now add correct answer on new branch
1214
+ * context.set(assistant('The answer is 4.'));
1215
+ * await context.save();
1216
+ * ```
1217
+ */
1218
+ async rewind(messageId) {
1219
+ await this.#ensureInitialized();
1220
+ const message2 = await this.#store.getMessage(messageId);
1221
+ if (!message2) {
1222
+ throw new Error(`Message "${messageId}" not found`);
1223
+ }
1224
+ if (message2.chatId !== this.#chatId) {
1225
+ throw new Error(`Message "${messageId}" belongs to a different chat`);
1226
+ }
1227
+ return this.#createBranchFrom(messageId, true);
1176
1228
  }
1177
- async getCheckpoint(chatId, name) {
1178
- const row = this.#db.prepare("SELECT * FROM checkpoints WHERE chatId = ? AND name = ?").get(chatId, name);
1179
- if (!row) {
1180
- return void 0;
1229
+ /**
1230
+ * Create a checkpoint at the current position.
1231
+ *
1232
+ * A checkpoint is a named pointer to the current branch head.
1233
+ * Use restore() to return to this point later.
1234
+ *
1235
+ * @param name - Name for the checkpoint
1236
+ * @returns The checkpoint info
1237
+ *
1238
+ * @example
1239
+ * ```ts
1240
+ * context.set(user('I want to learn a new skill.'));
1241
+ * context.set(assistant('Would you like coding or cooking?'));
1242
+ * await context.save();
1243
+ *
1244
+ * // Save checkpoint before user's choice
1245
+ * const cp = await context.checkpoint('before-choice');
1246
+ * ```
1247
+ */
1248
+ async checkpoint(name) {
1249
+ await this.#ensureInitialized();
1250
+ if (!this.#branch?.headMessageId) {
1251
+ throw new Error("Cannot create checkpoint: no messages in conversation");
1181
1252
  }
1253
+ const checkpoint = {
1254
+ id: crypto.randomUUID(),
1255
+ chatId: this.#chatId,
1256
+ name,
1257
+ messageId: this.#branch.headMessageId,
1258
+ createdAt: Date.now()
1259
+ };
1260
+ await this.#store.createCheckpoint(checkpoint);
1182
1261
  return {
1183
- id: row.id,
1184
- chatId: row.chatId,
1185
- name: row.name,
1186
- messageId: row.messageId,
1187
- createdAt: row.createdAt
1262
+ id: checkpoint.id,
1263
+ name: checkpoint.name,
1264
+ messageId: checkpoint.messageId,
1265
+ createdAt: checkpoint.createdAt
1188
1266
  };
1189
1267
  }
1190
- async listCheckpoints(chatId) {
1191
- const rows = this.#db.prepare(
1192
- `SELECT id, name, messageId, createdAt
1193
- FROM checkpoints
1194
- WHERE chatId = ?
1195
- ORDER BY createdAt DESC`
1196
- ).all(chatId);
1197
- return rows.map((row) => ({
1198
- id: row.id,
1199
- name: row.name,
1200
- messageId: row.messageId,
1201
- createdAt: row.createdAt
1202
- }));
1268
+ /**
1269
+ * Restore to a checkpoint by creating a new branch from that point.
1270
+ *
1271
+ * @param name - Name of the checkpoint to restore
1272
+ * @returns The new branch info
1273
+ *
1274
+ * @example
1275
+ * ```ts
1276
+ * // User chose cooking, but wants to try coding path
1277
+ * await context.restore('before-choice');
1278
+ *
1279
+ * context.set(user('I want to learn coding.'));
1280
+ * context.set(assistant('Python is a great starting language!'));
1281
+ * await context.save();
1282
+ * ```
1283
+ */
1284
+ async restore(name) {
1285
+ await this.#ensureInitialized();
1286
+ const checkpoint = await this.#store.getCheckpoint(this.#chatId, name);
1287
+ if (!checkpoint) {
1288
+ throw new Error(
1289
+ `Checkpoint "${name}" not found in chat "${this.#chatId}"`
1290
+ );
1291
+ }
1292
+ return this.rewind(checkpoint.messageId);
1293
+ }
1294
+ /**
1295
+ * Switch to a different branch by name.
1296
+ *
1297
+ * @param name - Branch name to switch to
1298
+ *
1299
+ * @example
1300
+ * ```ts
1301
+ * // List branches (via store)
1302
+ * const branches = await store.listBranches(context.chatId);
1303
+ * console.log(branches); // [{name: 'main', ...}, {name: 'main-v2', ...}]
1304
+ *
1305
+ * // Switch to original branch
1306
+ * await context.switchBranch('main');
1307
+ * ```
1308
+ */
1309
+ async switchBranch(name) {
1310
+ await this.#ensureInitialized();
1311
+ const branch = await this.#store.getBranch(this.#chatId, name);
1312
+ if (!branch) {
1313
+ throw new Error(`Branch "${name}" not found in chat "${this.#chatId}"`);
1314
+ }
1315
+ await this.#store.setActiveBranch(this.#chatId, branch.id);
1316
+ this.#branch = { ...branch, isActive: true };
1317
+ this.#branchName = name;
1318
+ this.#pendingMessages = [];
1319
+ }
1320
+ /**
1321
+ * Create a parallel branch from the current position ("by the way").
1322
+ *
1323
+ * Use this when you want to fork the conversation without leaving
1324
+ * the current branch. Common use case: user wants to ask another
1325
+ * question while waiting for the model to respond.
1326
+ *
1327
+ * Unlike rewind(), this method:
1328
+ * - Uses the current HEAD (no messageId needed)
1329
+ * - Does NOT switch to the new branch
1330
+ * - Keeps pending messages intact
1331
+ *
1332
+ * @returns The new branch info (does not switch to it)
1333
+ * @throws Error if no messages exist in the conversation
1334
+ *
1335
+ * @example
1336
+ * ```ts
1337
+ * // User asked a question, model is generating...
1338
+ * context.set(user('What is the weather?'));
1339
+ * await context.save();
1340
+ *
1341
+ * // User wants to ask something else without waiting
1342
+ * const newBranch = await context.btw();
1343
+ * // newBranch = { name: 'main-v2', ... }
1344
+ *
1345
+ * // Later, switch to the new branch and add the question
1346
+ * await context.switchBranch(newBranch.name);
1347
+ * context.set(user('Also, what time is it?'));
1348
+ * await context.save();
1349
+ * ```
1350
+ */
1351
+ async btw() {
1352
+ await this.#ensureInitialized();
1353
+ if (!this.#branch?.headMessageId) {
1354
+ throw new Error("Cannot create btw branch: no messages in conversation");
1355
+ }
1356
+ return this.#createBranchFrom(this.#branch.headMessageId, false);
1357
+ }
1358
+ /**
1359
+ * Update metadata for the current chat.
1360
+ *
1361
+ * @param updates - Partial metadata to merge (title, metadata)
1362
+ *
1363
+ * @example
1364
+ * ```ts
1365
+ * await context.updateChat({
1366
+ * title: 'Coding Help Session',
1367
+ * metadata: { tags: ['python', 'debugging'] }
1368
+ * });
1369
+ * ```
1370
+ */
1371
+ async updateChat(updates) {
1372
+ await this.#ensureInitialized();
1373
+ const storeUpdates = {};
1374
+ if (updates.title !== void 0) {
1375
+ storeUpdates.title = updates.title;
1376
+ }
1377
+ if (updates.metadata !== void 0) {
1378
+ storeUpdates.metadata = {
1379
+ ...this.#chatData?.metadata,
1380
+ ...updates.metadata
1381
+ };
1382
+ }
1383
+ this.#chatData = await this.#store.updateChat(this.#chatId, storeUpdates);
1384
+ }
1385
+ /**
1386
+ * Consolidate context fragments (no-op for now).
1387
+ *
1388
+ * This is a placeholder for future functionality that merges context fragments
1389
+ * using specific rules. Currently, it does nothing.
1390
+ *
1391
+ * @experimental
1392
+ */
1393
+ consolidate() {
1394
+ return void 0;
1395
+ }
1396
+ /**
1397
+ * Inspect the full context state for debugging.
1398
+ * Returns a comprehensive JSON-serializable object with all context information.
1399
+ *
1400
+ * @param options - Inspection options (modelId and renderer required)
1401
+ * @returns Complete inspection data including estimates, rendered output, fragments, and graph
1402
+ *
1403
+ * @example
1404
+ * ```ts
1405
+ * const inspection = await context.inspect({
1406
+ * modelId: 'openai:gpt-4o',
1407
+ * renderer: new XmlRenderer(),
1408
+ * });
1409
+ * console.log(JSON.stringify(inspection, null, 2));
1410
+ *
1411
+ * // Or write to file for analysis
1412
+ * await fs.writeFile('context-debug.json', JSON.stringify(inspection, null, 2));
1413
+ * ```
1414
+ */
1415
+ async inspect(options) {
1416
+ await this.#ensureInitialized();
1417
+ const { renderer } = options;
1418
+ const estimateResult = await this.estimate(options.modelId, { renderer });
1419
+ const rendered = renderer.render(this.#fragments);
1420
+ const persistedMessages = [];
1421
+ if (this.#branch?.headMessageId) {
1422
+ const chain = await this.#store.getMessageChain(
1423
+ this.#branch.headMessageId
1424
+ );
1425
+ persistedMessages.push(...chain);
1426
+ }
1427
+ const graph = await this.#store.getGraph(this.#chatId);
1428
+ return {
1429
+ estimate: estimateResult,
1430
+ rendered,
1431
+ fragments: {
1432
+ context: [...this.#fragments],
1433
+ pending: [...this.#pendingMessages],
1434
+ persisted: persistedMessages
1435
+ },
1436
+ graph,
1437
+ meta: {
1438
+ chatId: this.#chatId,
1439
+ branch: this.#branchName,
1440
+ timestamp: Date.now()
1441
+ }
1442
+ };
1443
+ }
1444
+ };
1445
+
1446
+ // packages/context/src/lib/fragments/domain.ts
1447
+ function term(name, definition) {
1448
+ return {
1449
+ name: "term",
1450
+ data: { name, definition }
1451
+ };
1452
+ }
1453
+ function hint(text) {
1454
+ return {
1455
+ name: "hint",
1456
+ data: text
1457
+ };
1458
+ }
1459
+ function guardrail(input) {
1460
+ return {
1461
+ name: "guardrail",
1462
+ data: {
1463
+ rule: input.rule,
1464
+ ...input.reason && { reason: input.reason },
1465
+ ...input.action && { action: input.action }
1466
+ }
1467
+ };
1468
+ }
1469
+ function explain(input) {
1470
+ return {
1471
+ name: "explain",
1472
+ data: {
1473
+ concept: input.concept,
1474
+ explanation: input.explanation,
1475
+ ...input.therefore && { therefore: input.therefore }
1476
+ }
1477
+ };
1478
+ }
1479
+ function example(input) {
1480
+ return {
1481
+ name: "example",
1482
+ data: {
1483
+ question: input.question,
1484
+ answer: input.answer,
1485
+ ...input.note && { note: input.note }
1486
+ }
1487
+ };
1488
+ }
1489
+ function clarification(input) {
1490
+ return {
1491
+ name: "clarification",
1492
+ data: {
1493
+ when: input.when,
1494
+ ask: input.ask,
1495
+ reason: input.reason
1496
+ }
1497
+ };
1498
+ }
1499
+ function workflow(input) {
1500
+ return {
1501
+ name: "workflow",
1502
+ data: {
1503
+ task: input.task,
1504
+ steps: input.steps,
1505
+ ...input.triggers?.length && { triggers: input.triggers },
1506
+ ...input.notes && { notes: input.notes }
1507
+ }
1508
+ };
1509
+ }
1510
+ function quirk(input) {
1511
+ return {
1512
+ name: "quirk",
1513
+ data: {
1514
+ issue: input.issue,
1515
+ workaround: input.workaround
1516
+ }
1517
+ };
1518
+ }
1519
+ function styleGuide(input) {
1520
+ return {
1521
+ name: "styleGuide",
1522
+ data: {
1523
+ prefer: input.prefer,
1524
+ ...input.never && { never: input.never },
1525
+ ...input.always && { always: input.always }
1526
+ }
1527
+ };
1528
+ }
1529
+ function analogy(input) {
1530
+ return {
1531
+ name: "analogy",
1532
+ data: {
1533
+ concepts: input.concepts,
1534
+ relationship: input.relationship,
1535
+ ...input.insight && { insight: input.insight },
1536
+ ...input.therefore && { therefore: input.therefore },
1537
+ ...input.pitfall && { pitfall: input.pitfall }
1538
+ }
1539
+ };
1540
+ }
1541
+ function glossary(entries) {
1542
+ return {
1543
+ name: "glossary",
1544
+ data: Object.entries(entries).map(([term2, expression]) => ({
1545
+ term: term2,
1546
+ expression
1547
+ }))
1548
+ };
1549
+ }
1550
+
1551
+ // packages/context/src/lib/fragments/user.ts
1552
+ function identity(input) {
1553
+ return {
1554
+ name: "identity",
1555
+ data: {
1556
+ ...input.name && { name: input.name },
1557
+ ...input.role && { role: input.role }
1558
+ }
1559
+ };
1560
+ }
1561
+ function persona(input) {
1562
+ return {
1563
+ name: "persona",
1564
+ data: {
1565
+ name: input.name,
1566
+ role: input.role,
1567
+ ...input.tone && { tone: input.tone }
1568
+ }
1569
+ };
1570
+ }
1571
+ function alias(term2, meaning) {
1572
+ return {
1573
+ name: "alias",
1574
+ data: { term: term2, meaning }
1575
+ };
1576
+ }
1577
+ function preference(aspect, value) {
1578
+ return {
1579
+ name: "preference",
1580
+ data: { aspect, value }
1581
+ };
1582
+ }
1583
+ function userContext(description) {
1584
+ return {
1585
+ name: "userContext",
1586
+ data: description
1587
+ };
1588
+ }
1589
+ function correction(subject, clarification2) {
1590
+ return {
1591
+ name: "correction",
1592
+ data: { subject, clarification: clarification2 }
1593
+ };
1594
+ }
1595
+
1596
+ // packages/context/src/lib/guardrail.ts
1597
+ function pass(part) {
1598
+ return { type: "pass", part };
1599
+ }
1600
+ function fail(feedback) {
1601
+ return { type: "fail", feedback };
1602
+ }
1603
+ function runGuardrailChain(part, guardrails, context) {
1604
+ let currentPart = part;
1605
+ for (const guardrail2 of guardrails) {
1606
+ const result = guardrail2.handle(currentPart, context);
1607
+ if (result.type === "fail") {
1608
+ return result;
1609
+ }
1610
+ currentPart = result.part;
1611
+ }
1612
+ return pass(currentPart);
1613
+ }
1614
+
1615
+ // packages/context/src/lib/sandbox/binary-bridges.ts
1616
+ import { defineCommand } from "just-bash";
1617
+ import spawn from "nano-spawn";
1618
+ import * as path from "path";
1619
+ import { existsSync } from "fs";
1620
+ function createBinaryBridges(...binaries) {
1621
+ return binaries.map((input) => {
1622
+ const config = typeof input === "string" ? { name: input } : input;
1623
+ const { name, binaryPath = name, allowedArgs } = config;
1624
+ return defineCommand(name, async (args, ctx) => {
1625
+ if (allowedArgs) {
1626
+ const invalidArg = args.find((arg) => !allowedArgs.test(arg));
1627
+ if (invalidArg) {
1628
+ return {
1629
+ stdout: "",
1630
+ stderr: `${name}: argument '${invalidArg}' not allowed by security policy`,
1631
+ exitCode: 1
1632
+ };
1633
+ }
1634
+ }
1635
+ try {
1636
+ const realCwd = resolveRealCwd(ctx);
1637
+ const resolvedArgs = args.map((arg) => {
1638
+ if (arg.startsWith("-")) {
1639
+ return arg;
1640
+ }
1641
+ const hasExtension = path.extname(arg) !== "";
1642
+ const hasPathSep = arg.includes(path.sep) || arg.includes("/");
1643
+ const isRelative = arg.startsWith(".");
1644
+ if (hasExtension || hasPathSep || isRelative) {
1645
+ return path.resolve(realCwd, arg);
1646
+ }
1647
+ return arg;
1648
+ });
1649
+ const mergedEnv = {
1650
+ ...process.env,
1651
+ ...ctx.env,
1652
+ PATH: process.env.PATH
1653
+ // Always use host PATH for binary bridges
1654
+ };
1655
+ const result = await spawn(binaryPath, resolvedArgs, {
1656
+ cwd: realCwd,
1657
+ env: mergedEnv
1658
+ });
1659
+ return {
1660
+ stdout: result.stdout,
1661
+ stderr: result.stderr,
1662
+ exitCode: 0
1663
+ };
1664
+ } catch (error) {
1665
+ if (error && typeof error === "object" && "exitCode" in error) {
1666
+ const subprocessError = error;
1667
+ return {
1668
+ stdout: subprocessError.stdout ?? "",
1669
+ stderr: subprocessError.stderr ?? "",
1670
+ exitCode: subprocessError.exitCode ?? 1
1671
+ };
1672
+ }
1673
+ return {
1674
+ stdout: "",
1675
+ stderr: `${name}: ${error instanceof Error ? error.message : String(error)}`,
1676
+ exitCode: 127
1677
+ };
1678
+ }
1679
+ });
1680
+ });
1681
+ }
1682
+ function resolveRealCwd(ctx) {
1683
+ const fs2 = ctx.fs;
1684
+ let realCwd;
1685
+ if (fs2.root) {
1686
+ realCwd = path.join(fs2.root, ctx.cwd);
1687
+ } else if (typeof fs2.getMountPoint === "function" && typeof fs2.toRealPath === "function") {
1688
+ const real = fs2.toRealPath(ctx.cwd);
1689
+ realCwd = real ?? process.cwd();
1690
+ } else {
1691
+ realCwd = process.cwd();
1692
+ }
1693
+ if (!existsSync(realCwd)) {
1694
+ realCwd = process.cwd();
1695
+ }
1696
+ return realCwd;
1697
+ }
1698
+
1699
+ // packages/context/src/lib/sandbox/docker-sandbox.ts
1700
+ import "bash-tool";
1701
+ import spawn2 from "nano-spawn";
1702
+ import { createHash } from "node:crypto";
1703
+ import { existsSync as existsSync2, readFileSync } from "node:fs";
1704
+ var DockerSandboxError = class extends Error {
1705
+ containerId;
1706
+ constructor(message2, containerId) {
1707
+ super(message2);
1708
+ this.name = "DockerSandboxError";
1709
+ this.containerId = containerId;
1710
+ }
1711
+ };
1712
+ var DockerNotAvailableError = class extends DockerSandboxError {
1713
+ constructor() {
1714
+ super("Docker is not available. Ensure Docker daemon is running.");
1715
+ this.name = "DockerNotAvailableError";
1716
+ }
1717
+ };
1718
+ var ContainerCreationError = class extends DockerSandboxError {
1719
+ image;
1720
+ cause;
1721
+ constructor(message2, image, cause) {
1722
+ super(`Failed to create container from image "${image}": ${message2}`);
1723
+ this.name = "ContainerCreationError";
1724
+ this.image = image;
1725
+ this.cause = cause;
1726
+ }
1727
+ };
1728
+ var PackageInstallError = class extends DockerSandboxError {
1729
+ packages;
1730
+ image;
1731
+ packageManager;
1732
+ stderr;
1733
+ constructor(packages, image, packageManager, stderr, containerId) {
1734
+ super(
1735
+ `Package installation failed for [${packages.join(", ")}] using ${packageManager} on ${image}: ${stderr}`,
1736
+ containerId
1737
+ );
1738
+ this.name = "PackageInstallError";
1739
+ this.packages = packages;
1740
+ this.image = image;
1741
+ this.packageManager = packageManager;
1742
+ this.stderr = stderr;
1743
+ }
1744
+ };
1745
+ var BinaryInstallError = class extends DockerSandboxError {
1746
+ binaryName;
1747
+ url;
1748
+ reason;
1749
+ constructor(binaryName, url, reason, containerId) {
1750
+ super(
1751
+ `Failed to install binary "${binaryName}" from ${url}: ${reason}`,
1752
+ containerId
1753
+ );
1754
+ this.name = "BinaryInstallError";
1755
+ this.binaryName = binaryName;
1756
+ this.url = url;
1757
+ this.reason = reason;
1758
+ }
1759
+ };
1760
+ var MountPathError = class extends DockerSandboxError {
1761
+ hostPath;
1762
+ containerPath;
1763
+ constructor(hostPath, containerPath) {
1764
+ super(
1765
+ `Mount path does not exist on host: "${hostPath}" -> "${containerPath}"`
1766
+ );
1767
+ this.name = "MountPathError";
1768
+ this.hostPath = hostPath;
1769
+ this.containerPath = containerPath;
1770
+ }
1771
+ };
1772
+ var DockerfileBuildError = class extends DockerSandboxError {
1773
+ stderr;
1774
+ constructor(stderr) {
1775
+ super(`Dockerfile build failed: ${stderr}`);
1776
+ this.name = "DockerfileBuildError";
1777
+ this.stderr = stderr;
1778
+ }
1779
+ };
1780
+ var ComposeStartError = class extends DockerSandboxError {
1781
+ composeFile;
1782
+ stderr;
1783
+ constructor(composeFile, stderr) {
1784
+ super(`Docker Compose failed to start: ${stderr}`);
1785
+ this.name = "ComposeStartError";
1786
+ this.composeFile = composeFile;
1787
+ this.stderr = stderr;
1788
+ }
1789
+ };
1790
+ function isDebianBased(image) {
1791
+ const debianPatterns = ["debian", "ubuntu", "node", "python"];
1792
+ return debianPatterns.some(
1793
+ (pattern) => image.toLowerCase().includes(pattern)
1794
+ );
1795
+ }
1796
+ function isDockerfileOptions(opts) {
1797
+ return "dockerfile" in opts;
1798
+ }
1799
+ function isComposeOptions(opts) {
1800
+ return "compose" in opts;
1801
+ }
1802
+ var DockerSandboxStrategy = class {
1803
+ context;
1804
+ mounts;
1805
+ resources;
1806
+ constructor(mounts = [], resources = {}) {
1807
+ this.mounts = mounts;
1808
+ this.resources = resources;
1809
+ }
1810
+ /**
1811
+ * Template method - defines the algorithm skeleton for creating a sandbox.
1812
+ *
1813
+ * Steps:
1814
+ * 1. Validate mount paths exist on host
1815
+ * 2. Get/build the Docker image (strategy-specific)
1816
+ * 3. Start the container
1817
+ * 4. Configure the container (strategy-specific)
1818
+ * 5. Create and return sandbox methods
1819
+ */
1820
+ async create() {
1821
+ this.validateMounts();
1822
+ const image = await this.getImage();
1823
+ const containerId = await this.startContainer(image);
1824
+ this.context = { containerId, image };
1825
+ try {
1826
+ await this.configure();
1827
+ } catch (error) {
1828
+ await this.stopContainer(containerId);
1829
+ throw error;
1830
+ }
1831
+ return this.createSandboxMethods();
1832
+ }
1833
+ // ─────────────────────────────────────────────────────────────────────────
1834
+ // Common implementations (shared by all strategies)
1835
+ // ─────────────────────────────────────────────────────────────────────────
1836
+ /**
1837
+ * Validates that all mount paths exist on the host filesystem.
1838
+ */
1839
+ validateMounts() {
1840
+ for (const mount of this.mounts) {
1841
+ if (!existsSync2(mount.hostPath)) {
1842
+ throw new MountPathError(mount.hostPath, mount.containerPath);
1843
+ }
1844
+ }
1845
+ }
1846
+ /**
1847
+ * Builds the docker run command arguments.
1848
+ */
1849
+ buildDockerArgs(image, containerId) {
1850
+ const { memory = "1g", cpus = 2 } = this.resources;
1851
+ const args = [
1852
+ "run",
1853
+ "-d",
1854
+ // Detached mode
1855
+ "--rm",
1856
+ // Remove container when stopped
1857
+ "--name",
1858
+ containerId,
1859
+ `--memory=${memory}`,
1860
+ `--cpus=${cpus}`,
1861
+ "-w",
1862
+ "/workspace"
1863
+ // Set working directory
1864
+ ];
1865
+ for (const mount of this.mounts) {
1866
+ const mode = mount.readOnly !== false ? "ro" : "rw";
1867
+ args.push("-v", `${mount.hostPath}:${mount.containerPath}:${mode}`);
1868
+ }
1869
+ args.push(image, "tail", "-f", "/dev/null");
1870
+ return args;
1871
+ }
1872
+ /**
1873
+ * Starts a Docker container with the given image.
1874
+ */
1875
+ async startContainer(image) {
1876
+ const containerId = `sandbox-${crypto.randomUUID().slice(0, 8)}`;
1877
+ const args = this.buildDockerArgs(image, containerId);
1878
+ try {
1879
+ await spawn2("docker", args);
1880
+ } catch (error) {
1881
+ const err = error;
1882
+ if (err.message?.includes("Cannot connect") || err.message?.includes("docker daemon") || err.stderr?.includes("Cannot connect")) {
1883
+ throw new DockerNotAvailableError();
1884
+ }
1885
+ throw new ContainerCreationError(err.message || String(err), image, err);
1886
+ }
1887
+ return containerId;
1888
+ }
1889
+ /**
1890
+ * Stops a Docker container.
1891
+ */
1892
+ async stopContainer(containerId) {
1893
+ try {
1894
+ await spawn2("docker", ["stop", containerId]);
1895
+ } catch {
1896
+ }
1897
+ }
1898
+ /**
1899
+ * Executes a command in the container.
1900
+ */
1901
+ async exec(command) {
1902
+ try {
1903
+ const result = await spawn2("docker", [
1904
+ "exec",
1905
+ this.context.containerId,
1906
+ "sh",
1907
+ "-c",
1908
+ command
1909
+ ]);
1910
+ return {
1911
+ stdout: result.stdout,
1912
+ stderr: result.stderr,
1913
+ exitCode: 0
1914
+ };
1915
+ } catch (error) {
1916
+ const err = error;
1917
+ return {
1918
+ stdout: err.stdout || "",
1919
+ stderr: err.stderr || err.message || "",
1920
+ exitCode: err.exitCode ?? 1
1921
+ };
1922
+ }
1923
+ }
1924
+ /**
1925
+ * Creates the DockerSandbox interface with all methods.
1926
+ */
1927
+ createSandboxMethods() {
1928
+ const { containerId } = this.context;
1929
+ const sandbox = {
1930
+ executeCommand: async (command) => {
1931
+ return this.exec(command);
1932
+ },
1933
+ readFile: async (path3) => {
1934
+ const result = await sandbox.executeCommand(`base64 "${path3}"`);
1935
+ if (result.exitCode !== 0) {
1936
+ throw new Error(`Failed to read file "${path3}": ${result.stderr}`);
1937
+ }
1938
+ return Buffer.from(result.stdout, "base64").toString("utf-8");
1939
+ },
1940
+ writeFiles: async (files) => {
1941
+ for (const file of files) {
1942
+ const dir = file.path.substring(0, file.path.lastIndexOf("/"));
1943
+ if (dir) {
1944
+ await sandbox.executeCommand(`mkdir -p "${dir}"`);
1945
+ }
1946
+ const base64Content = Buffer.from(file.content).toString("base64");
1947
+ const result = await sandbox.executeCommand(
1948
+ `echo "${base64Content}" | base64 -d > "${file.path}"`
1949
+ );
1950
+ if (result.exitCode !== 0) {
1951
+ throw new Error(
1952
+ `Failed to write file "${file.path}": ${result.stderr}`
1953
+ );
1954
+ }
1955
+ }
1956
+ },
1957
+ dispose: async () => {
1958
+ await this.stopContainer(containerId);
1959
+ }
1960
+ };
1961
+ return sandbox;
1962
+ }
1963
+ };
1964
+ var RuntimeStrategy = class extends DockerSandboxStrategy {
1965
+ image;
1966
+ packages;
1967
+ binaries;
1968
+ constructor(image = "alpine:latest", packages = [], binaries = [], mounts, resources) {
1969
+ super(mounts, resources);
1970
+ this.image = image;
1971
+ this.packages = packages;
1972
+ this.binaries = binaries;
1973
+ }
1974
+ async getImage() {
1975
+ return this.image;
1976
+ }
1977
+ async configure() {
1978
+ await this.installPackages();
1979
+ await this.installBinaries();
1980
+ }
1981
+ /**
1982
+ * Installs packages using the appropriate package manager (apk/apt-get).
1983
+ */
1984
+ async installPackages() {
1985
+ if (this.packages.length === 0) return;
1986
+ const useApt = isDebianBased(this.image);
1987
+ const installCmd = useApt ? `apt-get update && apt-get install -y ${this.packages.join(" ")}` : `apk add --no-cache ${this.packages.join(" ")}`;
1988
+ try {
1989
+ await spawn2("docker", [
1990
+ "exec",
1991
+ this.context.containerId,
1992
+ "sh",
1993
+ "-c",
1994
+ installCmd
1995
+ ]);
1996
+ } catch (error) {
1997
+ const err = error;
1998
+ throw new PackageInstallError(
1999
+ this.packages,
2000
+ this.image,
2001
+ useApt ? "apt-get" : "apk",
2002
+ err.stderr || err.message,
2003
+ this.context.containerId
2004
+ );
2005
+ }
2006
+ }
2007
+ /**
2008
+ * Installs binaries from URLs.
2009
+ */
2010
+ async installBinaries() {
2011
+ if (this.binaries.length === 0) return;
2012
+ await this.ensureCurl();
2013
+ const arch = await this.detectArchitecture();
2014
+ for (const binary of this.binaries) {
2015
+ await this.installBinary(binary, arch);
2016
+ }
2017
+ }
2018
+ /**
2019
+ * Ensures curl is installed in the container.
2020
+ */
2021
+ async ensureCurl() {
2022
+ const checkResult = await spawn2("docker", [
2023
+ "exec",
2024
+ this.context.containerId,
2025
+ "which",
2026
+ "curl"
2027
+ ]).catch(() => null);
2028
+ if (checkResult) return;
2029
+ const useApt = isDebianBased(this.image);
2030
+ const curlInstallCmd = useApt ? "apt-get update && apt-get install -y curl" : "apk add --no-cache curl";
2031
+ try {
2032
+ await spawn2("docker", [
2033
+ "exec",
2034
+ this.context.containerId,
2035
+ "sh",
2036
+ "-c",
2037
+ curlInstallCmd
2038
+ ]);
2039
+ } catch (error) {
2040
+ const err = error;
2041
+ throw new BinaryInstallError(
2042
+ "curl",
2043
+ "package-manager",
2044
+ `Required for binary downloads: ${err.stderr || err.message}`,
2045
+ this.context.containerId
2046
+ );
2047
+ }
2048
+ }
2049
+ /**
2050
+ * Detects the container's CPU architecture.
2051
+ */
2052
+ async detectArchitecture() {
2053
+ try {
2054
+ const result = await spawn2("docker", [
2055
+ "exec",
2056
+ this.context.containerId,
2057
+ "uname",
2058
+ "-m"
2059
+ ]);
2060
+ return result.stdout.trim();
2061
+ } catch (error) {
2062
+ const err = error;
2063
+ throw new DockerSandboxError(
2064
+ `Failed to detect container architecture: ${err.stderr || err.message}`,
2065
+ this.context.containerId
2066
+ );
2067
+ }
2068
+ }
2069
+ /**
2070
+ * Installs a single binary from URL.
2071
+ */
2072
+ async installBinary(binary, arch) {
2073
+ let url;
2074
+ if (typeof binary.url === "string") {
2075
+ url = binary.url;
2076
+ } else {
2077
+ const archUrl = binary.url[arch];
2078
+ if (!archUrl) {
2079
+ throw new BinaryInstallError(
2080
+ binary.name,
2081
+ `arch:${arch}`,
2082
+ `No URL provided for architecture "${arch}". Available: ${Object.keys(binary.url).join(", ")}`,
2083
+ this.context.containerId
2084
+ );
2085
+ }
2086
+ url = archUrl;
2087
+ }
2088
+ const isTarGz = url.endsWith(".tar.gz") || url.endsWith(".tgz");
2089
+ let installCmd;
2090
+ if (isTarGz) {
2091
+ const binaryPathInArchive = binary.binaryPath || binary.name;
2092
+ installCmd = `
2093
+ set -e
2094
+ TMPDIR=$(mktemp -d)
2095
+ cd "$TMPDIR"
2096
+ curl -fsSL "${url}" -o archive.tar.gz
2097
+ tar -xzf archive.tar.gz
2098
+ BINARY_FILE=$(find . -name "${binaryPathInArchive}" -o -name "${binary.name}" | head -1)
2099
+ if [ -z "$BINARY_FILE" ]; then
2100
+ echo "Binary not found in archive. Contents:" >&2
2101
+ find . -type f >&2
2102
+ exit 1
2103
+ fi
2104
+ chmod +x "$BINARY_FILE"
2105
+ mv "$BINARY_FILE" /usr/local/bin/${binary.name}
2106
+ cd /
2107
+ rm -rf "$TMPDIR"
2108
+ `;
2109
+ } else {
2110
+ installCmd = `
2111
+ curl -fsSL "${url}" -o /usr/local/bin/${binary.name}
2112
+ chmod +x /usr/local/bin/${binary.name}
2113
+ `;
2114
+ }
2115
+ try {
2116
+ await spawn2("docker", [
2117
+ "exec",
2118
+ this.context.containerId,
2119
+ "sh",
2120
+ "-c",
2121
+ installCmd
2122
+ ]);
2123
+ } catch (error) {
2124
+ const err = error;
2125
+ throw new BinaryInstallError(
2126
+ binary.name,
2127
+ url,
2128
+ err.stderr || err.message,
2129
+ this.context.containerId
2130
+ );
2131
+ }
2132
+ }
2133
+ };
2134
+ var DockerfileStrategy = class extends DockerSandboxStrategy {
2135
+ imageTag;
2136
+ dockerfile;
2137
+ dockerContext;
2138
+ constructor(dockerfile, dockerContext = ".", mounts, resources) {
2139
+ super(mounts, resources);
2140
+ this.dockerfile = dockerfile;
2141
+ this.dockerContext = dockerContext;
2142
+ this.imageTag = this.computeImageTag();
2143
+ }
2144
+ /**
2145
+ * Computes a deterministic image tag based on Dockerfile content.
2146
+ * Same Dockerfile → same tag → Docker skips rebuild if image exists.
2147
+ */
2148
+ computeImageTag() {
2149
+ const content = this.isInlineDockerfile() ? this.dockerfile : readFileSync(this.dockerfile, "utf-8");
2150
+ const hash = createHash("sha256").update(content).digest("hex").slice(0, 12);
2151
+ return `sandbox-${hash}`;
2152
+ }
2153
+ /**
2154
+ * Checks if the dockerfile property is inline content or a file path.
2155
+ */
2156
+ isInlineDockerfile() {
2157
+ return this.dockerfile.includes("\n");
2158
+ }
2159
+ async getImage() {
2160
+ const exists = await this.imageExists();
2161
+ if (!exists) {
2162
+ await this.buildImage();
2163
+ }
2164
+ return this.imageTag;
1203
2165
  }
1204
- async deleteCheckpoint(chatId, name) {
1205
- this.#db.prepare("DELETE FROM checkpoints WHERE chatId = ? AND name = ?").run(chatId, name);
2166
+ async configure() {
1206
2167
  }
1207
- // ==========================================================================
1208
- // Search Operations
1209
- // ==========================================================================
1210
- async searchMessages(chatId, query, options) {
1211
- const limit = options?.limit ?? 20;
1212
- const roles = options?.roles;
1213
- let sql = `
1214
- SELECT
1215
- m.id,
1216
- m.chatId,
1217
- m.parentId,
1218
- m.name,
1219
- m.type,
1220
- m.data,
1221
- m.createdAt,
1222
- fts.rank,
1223
- snippet(messages_fts, 3, '<mark>', '</mark>', '...', 32) as snippet
1224
- FROM messages_fts fts
1225
- JOIN messages m ON m.id = fts.messageId
1226
- WHERE messages_fts MATCH ?
1227
- AND fts.chatId = ?
1228
- `;
1229
- const params = [query, chatId];
1230
- if (roles && roles.length > 0) {
1231
- const placeholders = roles.map(() => "?").join(", ");
1232
- sql += ` AND fts.name IN (${placeholders})`;
1233
- params.push(...roles);
2168
+ /**
2169
+ * Checks if the image already exists locally.
2170
+ */
2171
+ async imageExists() {
2172
+ try {
2173
+ await spawn2("docker", ["image", "inspect", this.imageTag]);
2174
+ return true;
2175
+ } catch {
2176
+ return false;
1234
2177
  }
1235
- sql += " ORDER BY fts.rank LIMIT ?";
1236
- params.push(limit);
1237
- const rows = this.#db.prepare(sql).all(...params);
1238
- return rows.map((row) => ({
1239
- message: {
1240
- id: row.id,
1241
- chatId: row.chatId,
1242
- parentId: row.parentId,
1243
- name: row.name,
1244
- type: row.type ?? void 0,
1245
- data: JSON.parse(row.data),
1246
- createdAt: row.createdAt
1247
- },
1248
- rank: row.rank,
1249
- snippet: row.snippet
1250
- }));
1251
2178
  }
1252
- // ==========================================================================
1253
- // Visualization Operations
1254
- // ==========================================================================
1255
- async getGraph(chatId) {
1256
- const messageRows = this.#db.prepare(
1257
- `SELECT id, parentId, name, data, createdAt
1258
- FROM messages
1259
- WHERE chatId = ?
1260
- ORDER BY createdAt ASC`
1261
- ).all(chatId);
1262
- const nodes = messageRows.map((row) => {
1263
- const data = JSON.parse(row.data);
1264
- const content = typeof data === "string" ? data : JSON.stringify(data);
2179
+ /**
2180
+ * Builds the Docker image from the Dockerfile.
2181
+ */
2182
+ async buildImage() {
2183
+ try {
2184
+ if (this.isInlineDockerfile()) {
2185
+ const buildCmd = `echo '${this.dockerfile.replace(/'/g, "'\\''")}' | docker build -t ${this.imageTag} -f - ${this.dockerContext}`;
2186
+ await spawn2("sh", ["-c", buildCmd]);
2187
+ } else {
2188
+ await spawn2("docker", [
2189
+ "build",
2190
+ "-t",
2191
+ this.imageTag,
2192
+ "-f",
2193
+ this.dockerfile,
2194
+ this.dockerContext
2195
+ ]);
2196
+ }
2197
+ } catch (error) {
2198
+ const err = error;
2199
+ throw new DockerfileBuildError(err.stderr || err.message);
2200
+ }
2201
+ }
2202
+ };
2203
+ var ComposeStrategy = class extends DockerSandboxStrategy {
2204
+ projectName;
2205
+ composeFile;
2206
+ service;
2207
+ constructor(composeFile, service, resources) {
2208
+ super([], resources);
2209
+ this.composeFile = composeFile;
2210
+ this.service = service;
2211
+ this.projectName = this.computeProjectName();
2212
+ }
2213
+ /**
2214
+ * Deterministic project name based on compose file content for caching.
2215
+ * Same compose file → same project name → faster subsequent startups.
2216
+ */
2217
+ computeProjectName() {
2218
+ const content = readFileSync(this.composeFile, "utf-8");
2219
+ const hash = createHash("sha256").update(content).digest("hex").slice(0, 8);
2220
+ return `sandbox-${hash}`;
2221
+ }
2222
+ /**
2223
+ * Override: No image to get - compose manages its own images.
2224
+ */
2225
+ async getImage() {
2226
+ return "";
2227
+ }
2228
+ /**
2229
+ * Override: Start all services with docker compose up.
2230
+ */
2231
+ async startContainer(_image) {
2232
+ try {
2233
+ await spawn2("docker", [
2234
+ "compose",
2235
+ "-f",
2236
+ this.composeFile,
2237
+ "-p",
2238
+ this.projectName,
2239
+ "up",
2240
+ "-d"
2241
+ ]);
2242
+ } catch (error) {
2243
+ const err = error;
2244
+ if (err.stderr?.includes("Cannot connect")) {
2245
+ throw new DockerNotAvailableError();
2246
+ }
2247
+ throw new ComposeStartError(this.composeFile, err.stderr || err.message);
2248
+ }
2249
+ return this.projectName;
2250
+ }
2251
+ async configure() {
2252
+ }
2253
+ /**
2254
+ * Override: Execute commands in the target service.
2255
+ */
2256
+ async exec(command) {
2257
+ try {
2258
+ const result = await spawn2("docker", [
2259
+ "compose",
2260
+ "-f",
2261
+ this.composeFile,
2262
+ "-p",
2263
+ this.projectName,
2264
+ "exec",
2265
+ "-T",
2266
+ // -T disables pseudo-TTY
2267
+ this.service,
2268
+ "sh",
2269
+ "-c",
2270
+ command
2271
+ ]);
2272
+ return { stdout: result.stdout, stderr: result.stderr, exitCode: 0 };
2273
+ } catch (error) {
2274
+ const err = error;
1265
2275
  return {
1266
- id: row.id,
1267
- parentId: row.parentId,
1268
- role: row.name,
1269
- content: content.length > 50 ? content.slice(0, 50) + "..." : content,
1270
- createdAt: row.createdAt
2276
+ stdout: err.stdout || "",
2277
+ stderr: err.stderr || err.message || "",
2278
+ exitCode: err.exitCode ?? 1
1271
2279
  };
1272
- });
1273
- const branchRows = this.#db.prepare(
1274
- `SELECT name, headMessageId, isActive
1275
- FROM branches
1276
- WHERE chatId = ?
1277
- ORDER BY createdAt ASC`
1278
- ).all(chatId);
1279
- const branches = branchRows.map((row) => ({
1280
- name: row.name,
1281
- headMessageId: row.headMessageId,
1282
- isActive: row.isActive === 1
1283
- }));
1284
- const checkpointRows = this.#db.prepare(
1285
- `SELECT name, messageId
1286
- FROM checkpoints
1287
- WHERE chatId = ?
1288
- ORDER BY createdAt ASC`
1289
- ).all(chatId);
1290
- const checkpoints = checkpointRows.map((row) => ({
1291
- name: row.name,
1292
- messageId: row.messageId
1293
- }));
1294
- return {
1295
- chatId,
1296
- nodes,
1297
- branches,
1298
- checkpoints
1299
- };
2280
+ }
1300
2281
  }
1301
- };
1302
-
1303
- // packages/context/src/lib/store/memory.store.ts
1304
- var InMemoryContextStore = class extends SqliteContextStore {
1305
- constructor() {
1306
- super(":memory:");
2282
+ /**
2283
+ * Override: Stop all services with docker compose down.
2284
+ */
2285
+ async stopContainer(_containerId) {
2286
+ try {
2287
+ await spawn2("docker", [
2288
+ "compose",
2289
+ "-f",
2290
+ this.composeFile,
2291
+ "-p",
2292
+ this.projectName,
2293
+ "down"
2294
+ ]);
2295
+ } catch {
2296
+ }
1307
2297
  }
1308
2298
  };
2299
+ async function createDockerSandbox(options = {}) {
2300
+ let strategy;
2301
+ if (isComposeOptions(options)) {
2302
+ strategy = new ComposeStrategy(
2303
+ options.compose,
2304
+ options.service,
2305
+ options.resources
2306
+ );
2307
+ } else if (isDockerfileOptions(options)) {
2308
+ strategy = new DockerfileStrategy(
2309
+ options.dockerfile,
2310
+ options.context,
2311
+ options.mounts,
2312
+ options.resources
2313
+ );
2314
+ } else {
2315
+ strategy = new RuntimeStrategy(
2316
+ options.image,
2317
+ options.packages,
2318
+ options.binaries,
2319
+ options.mounts,
2320
+ options.resources
2321
+ );
2322
+ }
2323
+ return strategy.create();
2324
+ }
2325
+ async function useSandbox(options, fn) {
2326
+ const sandbox = await createDockerSandbox(options);
2327
+ try {
2328
+ return await fn(sandbox);
2329
+ } finally {
2330
+ await sandbox.dispose();
2331
+ }
2332
+ }
1309
2333
 
1310
- // packages/context/src/lib/visualize.ts
1311
- function visualizeGraph(data) {
1312
- if (data.nodes.length === 0) {
1313
- return `[chat: ${data.chatId}]
2334
+ // packages/context/src/lib/sandbox/container-tool.ts
2335
+ import {
2336
+ createBashTool
2337
+ } from "bash-tool";
2338
+ async function createContainerTool(options = {}) {
2339
+ let sandboxOptions;
2340
+ let bashOptions;
2341
+ if (isComposeOptions(options)) {
2342
+ const { compose, service, resources, ...rest } = options;
2343
+ sandboxOptions = { compose, service, resources };
2344
+ bashOptions = rest;
2345
+ } else if (isDockerfileOptions(options)) {
2346
+ const { dockerfile, context, mounts, resources, ...rest } = options;
2347
+ sandboxOptions = { dockerfile, context, mounts, resources };
2348
+ bashOptions = rest;
2349
+ } else {
2350
+ const { image, packages, binaries, mounts, resources, ...rest } = options;
2351
+ sandboxOptions = { image, packages, binaries, mounts, resources };
2352
+ bashOptions = rest;
2353
+ }
2354
+ const sandbox = await createDockerSandbox(sandboxOptions);
2355
+ const toolkit = await createBashTool({
2356
+ ...bashOptions,
2357
+ sandbox
2358
+ });
2359
+ return {
2360
+ bash: toolkit.bash,
2361
+ tools: toolkit.tools,
2362
+ sandbox
2363
+ };
2364
+ }
1314
2365
 
1315
- (empty)`;
2366
+ // packages/context/src/lib/skills/loader.ts
2367
+ import * as fs from "node:fs";
2368
+ import * as path2 from "node:path";
2369
+ import YAML from "yaml";
2370
+ function parseFrontmatter(content) {
2371
+ const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/;
2372
+ const match = content.match(frontmatterRegex);
2373
+ if (!match) {
2374
+ throw new Error("Invalid SKILL.md: missing or malformed frontmatter");
2375
+ }
2376
+ const [, yamlContent, body] = match;
2377
+ const frontmatter = YAML.parse(yamlContent);
2378
+ if (!frontmatter.name || typeof frontmatter.name !== "string") {
2379
+ throw new Error('Invalid SKILL.md: frontmatter must have a "name" field');
2380
+ }
2381
+ if (!frontmatter.description || typeof frontmatter.description !== "string") {
2382
+ throw new Error('Invalid SKILL.md: frontmatter must have a "description" field');
1316
2383
  }
1317
- const childrenByParentId = /* @__PURE__ */ new Map();
1318
- const branchHeads = /* @__PURE__ */ new Map();
1319
- const checkpointsByMessageId = /* @__PURE__ */ new Map();
1320
- for (const node of data.nodes) {
1321
- const children = childrenByParentId.get(node.parentId) ?? [];
1322
- children.push(node);
1323
- childrenByParentId.set(node.parentId, children);
2384
+ return {
2385
+ frontmatter,
2386
+ body: body.trim()
2387
+ };
2388
+ }
2389
+ function loadSkillMetadata(skillMdPath) {
2390
+ const content = fs.readFileSync(skillMdPath, "utf-8");
2391
+ const parsed = parseFrontmatter(content);
2392
+ const skillDir = path2.dirname(skillMdPath);
2393
+ return {
2394
+ name: parsed.frontmatter.name,
2395
+ description: parsed.frontmatter.description,
2396
+ path: skillDir,
2397
+ skillMdPath
2398
+ };
2399
+ }
2400
+ function discoverSkillsInDirectory(directory) {
2401
+ const skills2 = [];
2402
+ const expandedDir = directory.startsWith("~") ? path2.join(process.env.HOME || "", directory.slice(1)) : directory;
2403
+ if (!fs.existsSync(expandedDir)) {
2404
+ return skills2;
2405
+ }
2406
+ const entries = fs.readdirSync(expandedDir, { withFileTypes: true });
2407
+ for (const entry of entries) {
2408
+ if (!entry.isDirectory()) continue;
2409
+ const skillMdPath = path2.join(expandedDir, entry.name, "SKILL.md");
2410
+ if (!fs.existsSync(skillMdPath)) continue;
2411
+ try {
2412
+ const metadata = loadSkillMetadata(skillMdPath);
2413
+ skills2.push(metadata);
2414
+ } catch (error) {
2415
+ console.warn(`Warning: Failed to load skill at ${skillMdPath}:`, error);
2416
+ }
2417
+ }
2418
+ return skills2;
2419
+ }
2420
+
2421
+ // packages/context/src/lib/skills/fragments.ts
2422
+ function skills(options) {
2423
+ const skillsMap = /* @__PURE__ */ new Map();
2424
+ for (const dir of options.paths) {
2425
+ const discovered = discoverSkillsInDirectory(dir);
2426
+ for (const skill of discovered) {
2427
+ skillsMap.set(skill.name, skill);
2428
+ }
2429
+ }
2430
+ const allSkills = Array.from(skillsMap.values());
2431
+ let filteredSkills = allSkills;
2432
+ if (options.include) {
2433
+ filteredSkills = allSkills.filter((s) => options.include.includes(s.name));
2434
+ }
2435
+ if (options.exclude) {
2436
+ filteredSkills = filteredSkills.filter(
2437
+ (s) => !options.exclude.includes(s.name)
2438
+ );
1324
2439
  }
1325
- for (const branch of data.branches) {
1326
- if (branch.headMessageId) {
1327
- const heads = branchHeads.get(branch.headMessageId) ?? [];
1328
- heads.push(branch.isActive ? `${branch.name} *` : branch.name);
1329
- branchHeads.set(branch.headMessageId, heads);
2440
+ const skillFragments = filteredSkills.map((skill) => ({
2441
+ name: "skill",
2442
+ data: {
2443
+ name: skill.name,
2444
+ path: skill.skillMdPath,
2445
+ description: skill.description
1330
2446
  }
2447
+ }));
2448
+ return {
2449
+ name: "available_skills",
2450
+ data: [
2451
+ {
2452
+ name: "instructions",
2453
+ data: SKILLS_INSTRUCTIONS
2454
+ },
2455
+ ...skillFragments
2456
+ ]
2457
+ };
2458
+ }
2459
+ var SKILLS_INSTRUCTIONS = `When a user's request matches one of the skills listed below, read the skill's SKILL.md file to get detailed instructions before proceeding. Skills provide specialized knowledge and workflows for specific tasks.
2460
+
2461
+ To use a skill:
2462
+ 1. Identify if the user's request matches a skill's description
2463
+ 2. Read the SKILL.md file at the skill's path to load full instructions
2464
+ 3. Follow the skill's guidance to complete the task
2465
+
2466
+ Skills are only loaded when relevant - don't read skill files unless needed.`;
2467
+
2468
+ // packages/context/src/lib/store/sqlite.store.ts
2469
+ import { DatabaseSync } from "node:sqlite";
2470
+ var STORE_DDL = `
2471
+ -- Chats table
2472
+ -- createdAt/updatedAt: DEFAULT for insert, inline SET for updates
2473
+ CREATE TABLE IF NOT EXISTS chats (
2474
+ id TEXT PRIMARY KEY,
2475
+ title TEXT,
2476
+ metadata TEXT,
2477
+ createdAt INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
2478
+ updatedAt INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)
2479
+ );
2480
+
2481
+ CREATE INDEX IF NOT EXISTS idx_chats_updatedAt ON chats(updatedAt);
2482
+
2483
+ -- Messages table (nodes in the DAG)
2484
+ CREATE TABLE IF NOT EXISTS messages (
2485
+ id TEXT PRIMARY KEY,
2486
+ chatId TEXT NOT NULL,
2487
+ parentId TEXT,
2488
+ name TEXT NOT NULL,
2489
+ type TEXT,
2490
+ data TEXT NOT NULL,
2491
+ createdAt INTEGER NOT NULL,
2492
+ FOREIGN KEY (chatId) REFERENCES chats(id) ON DELETE CASCADE,
2493
+ FOREIGN KEY (parentId) REFERENCES messages(id)
2494
+ );
2495
+
2496
+ CREATE INDEX IF NOT EXISTS idx_messages_chatId ON messages(chatId);
2497
+ CREATE INDEX IF NOT EXISTS idx_messages_parentId ON messages(parentId);
2498
+
2499
+ -- Branches table (pointers to head messages)
2500
+ CREATE TABLE IF NOT EXISTS branches (
2501
+ id TEXT PRIMARY KEY,
2502
+ chatId TEXT NOT NULL,
2503
+ name TEXT NOT NULL,
2504
+ headMessageId TEXT,
2505
+ isActive INTEGER NOT NULL DEFAULT 0,
2506
+ createdAt INTEGER NOT NULL,
2507
+ FOREIGN KEY (chatId) REFERENCES chats(id) ON DELETE CASCADE,
2508
+ FOREIGN KEY (headMessageId) REFERENCES messages(id),
2509
+ UNIQUE(chatId, name)
2510
+ );
2511
+
2512
+ CREATE INDEX IF NOT EXISTS idx_branches_chatId ON branches(chatId);
2513
+
2514
+ -- Checkpoints table (pointers to message nodes)
2515
+ CREATE TABLE IF NOT EXISTS checkpoints (
2516
+ id TEXT PRIMARY KEY,
2517
+ chatId TEXT NOT NULL,
2518
+ name TEXT NOT NULL,
2519
+ messageId TEXT NOT NULL,
2520
+ createdAt INTEGER NOT NULL,
2521
+ FOREIGN KEY (chatId) REFERENCES chats(id) ON DELETE CASCADE,
2522
+ FOREIGN KEY (messageId) REFERENCES messages(id),
2523
+ UNIQUE(chatId, name)
2524
+ );
2525
+
2526
+ CREATE INDEX IF NOT EXISTS idx_checkpoints_chatId ON checkpoints(chatId);
2527
+
2528
+ -- FTS5 virtual table for full-text search
2529
+ -- messageId/chatId/name are UNINDEXED (stored but not searchable, used for filtering/joining)
2530
+ -- Only 'content' is indexed for full-text search
2531
+ CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
2532
+ messageId UNINDEXED,
2533
+ chatId UNINDEXED,
2534
+ name UNINDEXED,
2535
+ content,
2536
+ tokenize='porter unicode61'
2537
+ );
2538
+ `;
2539
+ var SqliteContextStore = class extends ContextStore {
2540
+ #db;
2541
+ constructor(path3) {
2542
+ super();
2543
+ this.#db = new DatabaseSync(path3);
2544
+ this.#db.exec("PRAGMA foreign_keys = ON");
2545
+ this.#db.exec(STORE_DDL);
2546
+ }
2547
+ // ==========================================================================
2548
+ // Chat Operations
2549
+ // ==========================================================================
2550
+ async createChat(chat) {
2551
+ this.#db.prepare(
2552
+ `INSERT INTO chats (id, title, metadata)
2553
+ VALUES (?, ?, ?)`
2554
+ ).run(
2555
+ chat.id,
2556
+ chat.title ?? null,
2557
+ chat.metadata ? JSON.stringify(chat.metadata) : null
2558
+ );
1331
2559
  }
1332
- for (const checkpoint of data.checkpoints) {
1333
- const cps = checkpointsByMessageId.get(checkpoint.messageId) ?? [];
1334
- cps.push(checkpoint.name);
1335
- checkpointsByMessageId.set(checkpoint.messageId, cps);
2560
+ async upsertChat(chat) {
2561
+ const row = this.#db.prepare(
2562
+ `INSERT INTO chats (id, title, metadata)
2563
+ VALUES (?, ?, ?)
2564
+ ON CONFLICT(id) DO UPDATE SET id = excluded.id
2565
+ RETURNING *`
2566
+ ).get(
2567
+ chat.id,
2568
+ chat.title ?? null,
2569
+ chat.metadata ? JSON.stringify(chat.metadata) : null
2570
+ );
2571
+ return {
2572
+ id: row.id,
2573
+ title: row.title ?? void 0,
2574
+ metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
2575
+ createdAt: row.createdAt,
2576
+ updatedAt: row.updatedAt
2577
+ };
1336
2578
  }
1337
- const roots = childrenByParentId.get(null) ?? [];
1338
- const lines = [`[chat: ${data.chatId}]`, ""];
1339
- function renderNode(node, prefix, isLast, isRoot) {
1340
- const connector = isRoot ? "" : isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
1341
- const contentPreview = node.content.replace(/\n/g, " ");
1342
- let line = `${prefix}${connector}${node.id.slice(0, 8)} (${node.role}): "${contentPreview}"`;
1343
- const branches = branchHeads.get(node.id);
1344
- if (branches) {
1345
- line += ` <- [${branches.join(", ")}]`;
2579
+ async getChat(chatId) {
2580
+ const row = this.#db.prepare("SELECT * FROM chats WHERE id = ?").get(chatId);
2581
+ if (!row) {
2582
+ return void 0;
1346
2583
  }
1347
- const checkpoints = checkpointsByMessageId.get(node.id);
1348
- if (checkpoints) {
1349
- line += ` {${checkpoints.join(", ")}}`;
2584
+ return {
2585
+ id: row.id,
2586
+ title: row.title ?? void 0,
2587
+ metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
2588
+ createdAt: row.createdAt,
2589
+ updatedAt: row.updatedAt
2590
+ };
2591
+ }
2592
+ async updateChat(chatId, updates) {
2593
+ const setClauses = ["updatedAt = strftime('%s', 'now') * 1000"];
2594
+ const params = [];
2595
+ if (updates.title !== void 0) {
2596
+ setClauses.push("title = ?");
2597
+ params.push(updates.title ?? null);
1350
2598
  }
1351
- lines.push(line);
1352
- const children = childrenByParentId.get(node.id) ?? [];
1353
- const childPrefix = isRoot ? "" : prefix + (isLast ? " " : "\u2502 ");
1354
- for (let i = 0; i < children.length; i++) {
1355
- renderNode(children[i], childPrefix, i === children.length - 1, false);
2599
+ if (updates.metadata !== void 0) {
2600
+ setClauses.push("metadata = ?");
2601
+ params.push(JSON.stringify(updates.metadata));
1356
2602
  }
2603
+ params.push(chatId);
2604
+ const row = this.#db.prepare(
2605
+ `UPDATE chats SET ${setClauses.join(", ")} WHERE id = ? RETURNING *`
2606
+ ).get(...params);
2607
+ return {
2608
+ id: row.id,
2609
+ title: row.title ?? void 0,
2610
+ metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
2611
+ createdAt: row.createdAt,
2612
+ updatedAt: row.updatedAt
2613
+ };
1357
2614
  }
1358
- for (let i = 0; i < roots.length; i++) {
1359
- renderNode(roots[i], "", i === roots.length - 1, true);
1360
- }
1361
- lines.push("");
1362
- lines.push("Legend: * = active branch, {...} = checkpoint");
1363
- return lines.join("\n");
1364
- }
1365
-
1366
- // packages/context/src/index.ts
1367
- var ContextEngine = class {
1368
- /** Non-message fragments (role, hints, etc.) - not persisted in graph */
1369
- #fragments = [];
1370
- /** Pending message fragments to be added to graph */
1371
- #pendingMessages = [];
1372
- #store;
1373
- #chatId;
1374
- #branchName;
1375
- #branch = null;
1376
- #chatData = null;
1377
- #initialized = false;
1378
- constructor(options) {
1379
- if (!options.chatId) {
1380
- throw new Error("chatId is required");
1381
- }
1382
- this.#store = options.store;
1383
- this.#chatId = options.chatId;
1384
- this.#branchName = options.branch ?? "main";
2615
+ async listChats() {
2616
+ const rows = this.#db.prepare(
2617
+ `SELECT
2618
+ c.id,
2619
+ c.title,
2620
+ c.createdAt,
2621
+ c.updatedAt,
2622
+ COUNT(DISTINCT m.id) as messageCount,
2623
+ COUNT(DISTINCT b.id) as branchCount
2624
+ FROM chats c
2625
+ LEFT JOIN messages m ON m.chatId = c.id
2626
+ LEFT JOIN branches b ON b.chatId = c.id
2627
+ GROUP BY c.id
2628
+ ORDER BY c.updatedAt DESC`
2629
+ ).all();
2630
+ return rows.map((row) => ({
2631
+ id: row.id,
2632
+ title: row.title ?? void 0,
2633
+ messageCount: row.messageCount,
2634
+ branchCount: row.branchCount,
2635
+ createdAt: row.createdAt,
2636
+ updatedAt: row.updatedAt
2637
+ }));
1385
2638
  }
1386
- /**
1387
- * Initialize the chat and branch if they don't exist.
1388
- */
1389
- async #ensureInitialized() {
1390
- if (this.#initialized) {
1391
- return;
1392
- }
1393
- this.#chatData = await this.#store.upsertChat({ id: this.#chatId });
1394
- const existingBranch = await this.#store.getBranch(
1395
- this.#chatId,
1396
- this.#branchName
2639
+ // ==========================================================================
2640
+ // Message Operations (Graph Nodes)
2641
+ // ==========================================================================
2642
+ async addMessage(message2) {
2643
+ this.#db.prepare(
2644
+ `INSERT INTO messages (id, chatId, parentId, name, type, data, createdAt)
2645
+ VALUES (?, ?, ?, ?, ?, ?, ?)
2646
+ ON CONFLICT(id) DO UPDATE SET
2647
+ parentId = excluded.parentId,
2648
+ name = excluded.name,
2649
+ type = excluded.type,
2650
+ data = excluded.data`
2651
+ ).run(
2652
+ message2.id,
2653
+ message2.chatId,
2654
+ message2.parentId,
2655
+ message2.name,
2656
+ message2.type ?? null,
2657
+ JSON.stringify(message2.data),
2658
+ message2.createdAt
1397
2659
  );
1398
- if (existingBranch) {
1399
- this.#branch = existingBranch;
1400
- } else {
1401
- this.#branch = {
1402
- id: crypto.randomUUID(),
1403
- chatId: this.#chatId,
1404
- name: this.#branchName,
1405
- headMessageId: null,
1406
- isActive: true,
1407
- createdAt: Date.now()
1408
- };
1409
- await this.#store.createBranch(this.#branch);
1410
- }
1411
- this.#initialized = true;
2660
+ const content = typeof message2.data === "string" ? message2.data : JSON.stringify(message2.data);
2661
+ this.#db.prepare(`DELETE FROM messages_fts WHERE messageId = ?`).run(message2.id);
2662
+ this.#db.prepare(
2663
+ `INSERT INTO messages_fts(messageId, chatId, name, content)
2664
+ VALUES (?, ?, ?, ?)`
2665
+ ).run(message2.id, message2.chatId, message2.name, content);
1412
2666
  }
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 = [];
2667
+ async getMessage(messageId) {
2668
+ const row = this.#db.prepare("SELECT * FROM messages WHERE id = ?").get(messageId);
2669
+ if (!row) {
2670
+ return void 0;
1437
2671
  }
1438
- const chain = await this.#store.getMessageChain(messageId);
1439
2672
  return {
1440
- id: newBranch.id,
1441
- name: newBranch.name,
1442
- headMessageId: newBranch.headMessageId,
1443
- isActive: switchTo,
1444
- messageCount: chain.length,
1445
- createdAt: newBranch.createdAt
2673
+ id: row.id,
2674
+ chatId: row.chatId,
2675
+ parentId: row.parentId,
2676
+ name: row.name,
2677
+ type: row.type ?? void 0,
2678
+ data: JSON.parse(row.data),
2679
+ createdAt: row.createdAt
1446
2680
  };
1447
2681
  }
1448
- /**
1449
- * Get the current chat ID.
1450
- */
1451
- get chatId() {
1452
- return this.#chatId;
2682
+ async getMessageChain(headId) {
2683
+ const rows = this.#db.prepare(
2684
+ `WITH RECURSIVE chain AS (
2685
+ SELECT *, 0 as depth FROM messages WHERE id = ?
2686
+ UNION ALL
2687
+ SELECT m.*, c.depth + 1 FROM messages m
2688
+ INNER JOIN chain c ON m.id = c.parentId
2689
+ )
2690
+ SELECT * FROM chain
2691
+ ORDER BY depth DESC`
2692
+ ).all(headId);
2693
+ return rows.map((row) => ({
2694
+ id: row.id,
2695
+ chatId: row.chatId,
2696
+ parentId: row.parentId,
2697
+ name: row.name,
2698
+ type: row.type ?? void 0,
2699
+ data: JSON.parse(row.data),
2700
+ createdAt: row.createdAt
2701
+ }));
1453
2702
  }
1454
- /**
1455
- * Get the current branch name.
1456
- */
1457
- get branch() {
1458
- return this.#branchName;
2703
+ async hasChildren(messageId) {
2704
+ const row = this.#db.prepare(
2705
+ "SELECT EXISTS(SELECT 1 FROM messages WHERE parentId = ?) as hasChildren"
2706
+ ).get(messageId);
2707
+ return row.hasChildren === 1;
2708
+ }
2709
+ // ==========================================================================
2710
+ // Branch Operations
2711
+ // ==========================================================================
2712
+ async createBranch(branch) {
2713
+ this.#db.prepare(
2714
+ `INSERT INTO branches (id, chatId, name, headMessageId, isActive, createdAt)
2715
+ VALUES (?, ?, ?, ?, ?, ?)`
2716
+ ).run(
2717
+ branch.id,
2718
+ branch.chatId,
2719
+ branch.name,
2720
+ branch.headMessageId,
2721
+ branch.isActive ? 1 : 0,
2722
+ branch.createdAt
2723
+ );
1459
2724
  }
1460
- /**
1461
- * Get metadata for the current chat.
1462
- * Returns null if the chat hasn't been initialized yet.
1463
- */
1464
- get chat() {
1465
- if (!this.#chatData) {
1466
- return null;
2725
+ async getBranch(chatId, name) {
2726
+ const row = this.#db.prepare("SELECT * FROM branches WHERE chatId = ? AND name = ?").get(chatId, name);
2727
+ if (!row) {
2728
+ return void 0;
1467
2729
  }
1468
2730
  return {
1469
- id: this.#chatData.id,
1470
- createdAt: this.#chatData.createdAt,
1471
- updatedAt: this.#chatData.updatedAt,
1472
- title: this.#chatData.title,
1473
- metadata: this.#chatData.metadata
2731
+ id: row.id,
2732
+ chatId: row.chatId,
2733
+ name: row.name,
2734
+ headMessageId: row.headMessageId,
2735
+ isActive: row.isActive === 1,
2736
+ createdAt: row.createdAt
1474
2737
  };
1475
2738
  }
1476
- /**
1477
- * Add fragments to the context.
1478
- *
1479
- * - Message fragments (user/assistant) are queued for persistence
1480
- * - Non-message fragments (role/hint) are kept in memory for system prompt
1481
- */
1482
- set(...fragments) {
1483
- for (const fragment2 of fragments) {
1484
- if (isMessageFragment(fragment2)) {
1485
- this.#pendingMessages.push(fragment2);
1486
- } else {
1487
- this.#fragments.push(fragment2);
1488
- }
2739
+ async getActiveBranch(chatId) {
2740
+ const row = this.#db.prepare("SELECT * FROM branches WHERE chatId = ? AND isActive = 1").get(chatId);
2741
+ if (!row) {
2742
+ return void 0;
1489
2743
  }
1490
- return this;
2744
+ return {
2745
+ id: row.id,
2746
+ chatId: row.chatId,
2747
+ name: row.name,
2748
+ headMessageId: row.headMessageId,
2749
+ isActive: true,
2750
+ createdAt: row.createdAt
2751
+ };
1491
2752
  }
1492
- /**
1493
- * Render all fragments using the provided renderer.
1494
- * @internal Use resolve() instead for public API.
1495
- */
1496
- render(renderer) {
1497
- return renderer.render(this.#fragments);
2753
+ async setActiveBranch(chatId, branchId) {
2754
+ this.#db.prepare("UPDATE branches SET isActive = 0 WHERE chatId = ?").run(chatId);
2755
+ this.#db.prepare("UPDATE branches SET isActive = 1 WHERE id = ?").run(branchId);
1498
2756
  }
1499
- /**
1500
- * Resolve context into AI SDK-ready format.
1501
- *
1502
- * - Initializes chat and branch if needed
1503
- * - Loads message history from the graph (walking parent chain)
1504
- * - Separates context fragments for system prompt
1505
- * - Combines with pending messages
1506
- *
1507
- * @example
1508
- * ```ts
1509
- * const context = new ContextEngine({ store, chatId: 'chat-1' })
1510
- * .set(role('You are helpful'), user('Hello'));
1511
- *
1512
- * const { systemPrompt, messages } = await context.resolve();
1513
- * await generateText({ system: systemPrompt, messages });
1514
- * ```
1515
- */
1516
- async resolve(options) {
1517
- await this.#ensureInitialized();
1518
- const systemPrompt = options.renderer.render(this.#fragments);
1519
- const messages = [];
1520
- if (this.#branch?.headMessageId) {
1521
- const chain = await this.#store.getMessageChain(
1522
- this.#branch.headMessageId
1523
- );
1524
- for (const msg of chain) {
1525
- messages.push(message(msg.data).codec?.decode());
2757
+ async updateBranchHead(branchId, messageId) {
2758
+ this.#db.prepare("UPDATE branches SET headMessageId = ? WHERE id = ?").run(messageId, branchId);
2759
+ }
2760
+ async listBranches(chatId) {
2761
+ const branches = this.#db.prepare(
2762
+ `SELECT
2763
+ b.id,
2764
+ b.name,
2765
+ b.headMessageId,
2766
+ b.isActive,
2767
+ b.createdAt
2768
+ FROM branches b
2769
+ WHERE b.chatId = ?
2770
+ ORDER BY b.createdAt ASC`
2771
+ ).all(chatId);
2772
+ const result = [];
2773
+ for (const branch of branches) {
2774
+ let messageCount = 0;
2775
+ if (branch.headMessageId) {
2776
+ const countRow = this.#db.prepare(
2777
+ `WITH RECURSIVE chain AS (
2778
+ SELECT id, parentId FROM messages WHERE id = ?
2779
+ UNION ALL
2780
+ SELECT m.id, m.parentId FROM messages m
2781
+ INNER JOIN chain c ON m.id = c.parentId
2782
+ )
2783
+ SELECT COUNT(*) as count FROM chain`
2784
+ ).get(branch.headMessageId);
2785
+ messageCount = countRow.count;
1526
2786
  }
2787
+ result.push({
2788
+ id: branch.id,
2789
+ name: branch.name,
2790
+ headMessageId: branch.headMessageId,
2791
+ isActive: branch.isActive === 1,
2792
+ messageCount,
2793
+ createdAt: branch.createdAt
2794
+ });
1527
2795
  }
1528
- for (const fragment2 of this.#pendingMessages) {
1529
- const decoded = fragment2.codec.decode();
1530
- messages.push(decoded);
1531
- }
1532
- return { systemPrompt, messages };
2796
+ return result;
1533
2797
  }
1534
- /**
1535
- * Save pending messages to the graph.
1536
- *
1537
- * Each message is added as a node with parentId pointing to the previous message.
1538
- * The branch head is updated to point to the last message.
1539
- *
1540
- * @example
1541
- * ```ts
1542
- * context.set(user('Hello'));
1543
- * // AI responds...
1544
- * context.set(assistant('Hi there!'));
1545
- * await context.save(); // Persist to graph
1546
- * ```
1547
- */
1548
- async save() {
1549
- await this.#ensureInitialized();
1550
- if (this.#pendingMessages.length === 0) {
1551
- return;
1552
- }
1553
- let parentId = this.#branch.headMessageId;
1554
- const now = Date.now();
1555
- for (const fragment2 of this.#pendingMessages) {
1556
- const messageData = {
1557
- id: fragment2.id ?? crypto.randomUUID(),
1558
- chatId: this.#chatId,
1559
- parentId,
1560
- name: fragment2.name,
1561
- type: fragment2.type,
1562
- data: fragment2.codec.encode(),
1563
- createdAt: now
1564
- };
1565
- await this.#store.addMessage(messageData);
1566
- parentId = messageData.id;
1567
- }
1568
- await this.#store.updateBranchHead(this.#branch.id, parentId);
1569
- this.#branch.headMessageId = parentId;
1570
- this.#pendingMessages = [];
2798
+ // ==========================================================================
2799
+ // Checkpoint Operations
2800
+ // ==========================================================================
2801
+ async createCheckpoint(checkpoint) {
2802
+ this.#db.prepare(
2803
+ `INSERT INTO checkpoints (id, chatId, name, messageId, createdAt)
2804
+ VALUES (?, ?, ?, ?, ?)
2805
+ ON CONFLICT(chatId, name) DO UPDATE SET
2806
+ messageId = excluded.messageId,
2807
+ createdAt = excluded.createdAt`
2808
+ ).run(
2809
+ checkpoint.id,
2810
+ checkpoint.chatId,
2811
+ checkpoint.name,
2812
+ checkpoint.messageId,
2813
+ checkpoint.createdAt
2814
+ );
1571
2815
  }
1572
- /**
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)
1579
- *
1580
- * @param modelId - Model ID (e.g., "openai:gpt-4o", "anthropic:claude-3-5-sonnet")
1581
- * @param options - Optional settings
1582
- * @returns Estimate result with token counts, costs, and per-fragment breakdown
1583
- */
1584
- async estimate(modelId, options = {}) {
1585
- await this.#ensureInitialized();
1586
- const renderer = options.renderer ?? new XmlRenderer();
1587
- const registry = getModelsRegistry();
1588
- await registry.load();
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
- });
2816
+ async getCheckpoint(chatId, name) {
2817
+ const row = this.#db.prepare("SELECT * FROM checkpoints WHERE chatId = ? AND name = ?").get(chatId, name);
2818
+ if (!row) {
2819
+ return void 0;
1634
2820
  }
1635
- const totalTokens = fragmentEstimates.reduce((sum, f) => sum + f.tokens, 0);
1636
- const totalCost = fragmentEstimates.reduce((sum, f) => sum + f.cost, 0);
1637
2821
  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
2822
+ id: row.id,
2823
+ chatId: row.chatId,
2824
+ name: row.name,
2825
+ messageId: row.messageId,
2826
+ createdAt: row.createdAt
1648
2827
  };
1649
2828
  }
1650
- /**
1651
- * Rewind to a specific message by ID.
1652
- *
1653
- * Creates a new branch from that message, preserving the original branch.
1654
- * The new branch becomes active.
1655
- *
1656
- * @param messageId - The message ID to rewind to
1657
- * @returns The new branch info
1658
- *
1659
- * @example
1660
- * ```ts
1661
- * context.set(user('What is 2 + 2?', { id: 'q1' }));
1662
- * context.set(assistant('The answer is 5.', { id: 'wrong' })); // Oops!
1663
- * await context.save();
1664
- *
1665
- * // Rewind to the question, creates new branch
1666
- * const newBranch = await context.rewind('q1');
1667
- *
1668
- * // Now add correct answer on new branch
1669
- * context.set(assistant('The answer is 4.'));
1670
- * await context.save();
1671
- * ```
1672
- */
1673
- async rewind(messageId) {
1674
- await this.#ensureInitialized();
1675
- const message2 = await this.#store.getMessage(messageId);
1676
- if (!message2) {
1677
- throw new Error(`Message "${messageId}" not found`);
1678
- }
1679
- if (message2.chatId !== this.#chatId) {
1680
- throw new Error(`Message "${messageId}" belongs to a different chat`);
1681
- }
1682
- return this.#createBranchFrom(messageId, true);
2829
+ async listCheckpoints(chatId) {
2830
+ const rows = this.#db.prepare(
2831
+ `SELECT id, name, messageId, createdAt
2832
+ FROM checkpoints
2833
+ WHERE chatId = ?
2834
+ ORDER BY createdAt DESC`
2835
+ ).all(chatId);
2836
+ return rows.map((row) => ({
2837
+ id: row.id,
2838
+ name: row.name,
2839
+ messageId: row.messageId,
2840
+ createdAt: row.createdAt
2841
+ }));
1683
2842
  }
1684
- /**
1685
- * Create a checkpoint at the current position.
1686
- *
1687
- * A checkpoint is a named pointer to the current branch head.
1688
- * Use restore() to return to this point later.
1689
- *
1690
- * @param name - Name for the checkpoint
1691
- * @returns The checkpoint info
1692
- *
1693
- * @example
1694
- * ```ts
1695
- * context.set(user('I want to learn a new skill.'));
1696
- * context.set(assistant('Would you like coding or cooking?'));
1697
- * await context.save();
1698
- *
1699
- * // Save checkpoint before user's choice
1700
- * const cp = await context.checkpoint('before-choice');
1701
- * ```
1702
- */
1703
- async checkpoint(name) {
1704
- await this.#ensureInitialized();
1705
- if (!this.#branch?.headMessageId) {
1706
- throw new Error("Cannot create checkpoint: no messages in conversation");
2843
+ async deleteCheckpoint(chatId, name) {
2844
+ this.#db.prepare("DELETE FROM checkpoints WHERE chatId = ? AND name = ?").run(chatId, name);
2845
+ }
2846
+ // ==========================================================================
2847
+ // Search Operations
2848
+ // ==========================================================================
2849
+ async searchMessages(chatId, query, options) {
2850
+ const limit = options?.limit ?? 20;
2851
+ const roles = options?.roles;
2852
+ let sql = `
2853
+ SELECT
2854
+ m.id,
2855
+ m.chatId,
2856
+ m.parentId,
2857
+ m.name,
2858
+ m.type,
2859
+ m.data,
2860
+ m.createdAt,
2861
+ fts.rank,
2862
+ snippet(messages_fts, 3, '<mark>', '</mark>', '...', 32) as snippet
2863
+ FROM messages_fts fts
2864
+ JOIN messages m ON m.id = fts.messageId
2865
+ WHERE messages_fts MATCH ?
2866
+ AND fts.chatId = ?
2867
+ `;
2868
+ const params = [query, chatId];
2869
+ if (roles && roles.length > 0) {
2870
+ const placeholders = roles.map(() => "?").join(", ");
2871
+ sql += ` AND fts.name IN (${placeholders})`;
2872
+ params.push(...roles);
1707
2873
  }
1708
- const checkpoint = {
1709
- id: crypto.randomUUID(),
1710
- chatId: this.#chatId,
1711
- name,
1712
- messageId: this.#branch.headMessageId,
1713
- createdAt: Date.now()
1714
- };
1715
- await this.#store.createCheckpoint(checkpoint);
2874
+ sql += " ORDER BY fts.rank LIMIT ?";
2875
+ params.push(limit);
2876
+ const rows = this.#db.prepare(sql).all(...params);
2877
+ return rows.map((row) => ({
2878
+ message: {
2879
+ id: row.id,
2880
+ chatId: row.chatId,
2881
+ parentId: row.parentId,
2882
+ name: row.name,
2883
+ type: row.type ?? void 0,
2884
+ data: JSON.parse(row.data),
2885
+ createdAt: row.createdAt
2886
+ },
2887
+ rank: row.rank,
2888
+ snippet: row.snippet
2889
+ }));
2890
+ }
2891
+ // ==========================================================================
2892
+ // Visualization Operations
2893
+ // ==========================================================================
2894
+ async getGraph(chatId) {
2895
+ const messageRows = this.#db.prepare(
2896
+ `SELECT id, parentId, name, data, createdAt
2897
+ FROM messages
2898
+ WHERE chatId = ?
2899
+ ORDER BY createdAt ASC`
2900
+ ).all(chatId);
2901
+ const nodes = messageRows.map((row) => {
2902
+ const data = JSON.parse(row.data);
2903
+ const content = typeof data === "string" ? data : JSON.stringify(data);
2904
+ return {
2905
+ id: row.id,
2906
+ parentId: row.parentId,
2907
+ role: row.name,
2908
+ content: content.length > 50 ? content.slice(0, 50) + "..." : content,
2909
+ createdAt: row.createdAt
2910
+ };
2911
+ });
2912
+ const branchRows = this.#db.prepare(
2913
+ `SELECT name, headMessageId, isActive
2914
+ FROM branches
2915
+ WHERE chatId = ?
2916
+ ORDER BY createdAt ASC`
2917
+ ).all(chatId);
2918
+ const branches = branchRows.map((row) => ({
2919
+ name: row.name,
2920
+ headMessageId: row.headMessageId,
2921
+ isActive: row.isActive === 1
2922
+ }));
2923
+ const checkpointRows = this.#db.prepare(
2924
+ `SELECT name, messageId
2925
+ FROM checkpoints
2926
+ WHERE chatId = ?
2927
+ ORDER BY createdAt ASC`
2928
+ ).all(chatId);
2929
+ const checkpoints = checkpointRows.map((row) => ({
2930
+ name: row.name,
2931
+ messageId: row.messageId
2932
+ }));
1716
2933
  return {
1717
- id: checkpoint.id,
1718
- name: checkpoint.name,
1719
- messageId: checkpoint.messageId,
1720
- createdAt: checkpoint.createdAt
2934
+ chatId,
2935
+ nodes,
2936
+ branches,
2937
+ checkpoints
1721
2938
  };
1722
2939
  }
1723
- /**
1724
- * Restore to a checkpoint by creating a new branch from that point.
1725
- *
1726
- * @param name - Name of the checkpoint to restore
1727
- * @returns The new branch info
1728
- *
1729
- * @example
1730
- * ```ts
1731
- * // User chose cooking, but wants to try coding path
1732
- * await context.restore('before-choice');
1733
- *
1734
- * context.set(user('I want to learn coding.'));
1735
- * context.set(assistant('Python is a great starting language!'));
1736
- * await context.save();
1737
- * ```
1738
- */
1739
- async restore(name) {
1740
- await this.#ensureInitialized();
1741
- const checkpoint = await this.#store.getCheckpoint(this.#chatId, name);
1742
- if (!checkpoint) {
1743
- throw new Error(
1744
- `Checkpoint "${name}" not found in chat "${this.#chatId}"`
1745
- );
2940
+ };
2941
+
2942
+ // packages/context/src/lib/store/memory.store.ts
2943
+ var InMemoryContextStore = class extends SqliteContextStore {
2944
+ constructor() {
2945
+ super(":memory:");
2946
+ }
2947
+ };
2948
+
2949
+ // packages/context/src/lib/visualize.ts
2950
+ function visualizeGraph(data) {
2951
+ if (data.nodes.length === 0) {
2952
+ return `[chat: ${data.chatId}]
2953
+
2954
+ (empty)`;
2955
+ }
2956
+ const childrenByParentId = /* @__PURE__ */ new Map();
2957
+ const branchHeads = /* @__PURE__ */ new Map();
2958
+ const checkpointsByMessageId = /* @__PURE__ */ new Map();
2959
+ for (const node of data.nodes) {
2960
+ const children = childrenByParentId.get(node.parentId) ?? [];
2961
+ children.push(node);
2962
+ childrenByParentId.set(node.parentId, children);
2963
+ }
2964
+ for (const branch of data.branches) {
2965
+ if (branch.headMessageId) {
2966
+ const heads = branchHeads.get(branch.headMessageId) ?? [];
2967
+ heads.push(branch.isActive ? `${branch.name} *` : branch.name);
2968
+ branchHeads.set(branch.headMessageId, heads);
2969
+ }
2970
+ }
2971
+ for (const checkpoint of data.checkpoints) {
2972
+ const cps = checkpointsByMessageId.get(checkpoint.messageId) ?? [];
2973
+ cps.push(checkpoint.name);
2974
+ checkpointsByMessageId.set(checkpoint.messageId, cps);
2975
+ }
2976
+ const roots = childrenByParentId.get(null) ?? [];
2977
+ const lines = [`[chat: ${data.chatId}]`, ""];
2978
+ function renderNode(node, prefix, isLast, isRoot) {
2979
+ const connector = isRoot ? "" : isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
2980
+ const contentPreview = node.content.replace(/\n/g, " ");
2981
+ let line = `${prefix}${connector}${node.id.slice(0, 8)} (${node.role}): "${contentPreview}"`;
2982
+ const branches = branchHeads.get(node.id);
2983
+ if (branches) {
2984
+ line += ` <- [${branches.join(", ")}]`;
2985
+ }
2986
+ const checkpoints = checkpointsByMessageId.get(node.id);
2987
+ if (checkpoints) {
2988
+ line += ` {${checkpoints.join(", ")}}`;
2989
+ }
2990
+ lines.push(line);
2991
+ const children = childrenByParentId.get(node.id) ?? [];
2992
+ const childPrefix = isRoot ? "" : prefix + (isLast ? " " : "\u2502 ");
2993
+ for (let i = 0; i < children.length; i++) {
2994
+ renderNode(children[i], childPrefix, i === children.length - 1, false);
1746
2995
  }
1747
- return this.rewind(checkpoint.messageId);
1748
2996
  }
1749
- /**
1750
- * Switch to a different branch by name.
1751
- *
1752
- * @param name - Branch name to switch to
1753
- *
1754
- * @example
1755
- * ```ts
1756
- * // List branches (via store)
1757
- * const branches = await store.listBranches(context.chatId);
1758
- * console.log(branches); // [{name: 'main', ...}, {name: 'main-v2', ...}]
1759
- *
1760
- * // Switch to original branch
1761
- * await context.switchBranch('main');
1762
- * ```
1763
- */
1764
- async switchBranch(name) {
1765
- await this.#ensureInitialized();
1766
- const branch = await this.#store.getBranch(this.#chatId, name);
1767
- if (!branch) {
1768
- throw new Error(`Branch "${name}" not found in chat "${this.#chatId}"`);
2997
+ for (let i = 0; i < roots.length; i++) {
2998
+ renderNode(roots[i], "", i === roots.length - 1, true);
2999
+ }
3000
+ lines.push("");
3001
+ lines.push("Legend: * = active branch, {...} = checkpoint");
3002
+ return lines.join("\n");
3003
+ }
3004
+
3005
+ // packages/context/src/lib/agent.ts
3006
+ import {
3007
+ Output,
3008
+ convertToModelMessages,
3009
+ createUIMessageStream,
3010
+ generateId as generateId2,
3011
+ generateText,
3012
+ smoothStream,
3013
+ stepCountIs,
3014
+ streamText
3015
+ } from "ai";
3016
+ import chalk from "chalk";
3017
+ import "zod";
3018
+ import "@deepagents/agent";
3019
+ var Agent = class _Agent {
3020
+ #options;
3021
+ #guardrails = [];
3022
+ tools;
3023
+ constructor(options) {
3024
+ this.#options = options;
3025
+ this.tools = options.tools || {};
3026
+ this.#guardrails = options.guardrails || [];
3027
+ }
3028
+ async generate(contextVariables, config) {
3029
+ if (!this.#options.context) {
3030
+ throw new Error(`Agent ${this.#options.name} is missing a context.`);
1769
3031
  }
1770
- await this.#store.setActiveBranch(this.#chatId, branch.id);
1771
- this.#branch = { ...branch, isActive: true };
1772
- this.#branchName = name;
1773
- this.#pendingMessages = [];
3032
+ if (!this.#options.model) {
3033
+ throw new Error(`Agent ${this.#options.name} is missing a model.`);
3034
+ }
3035
+ const { messages, systemPrompt } = await this.#options.context.resolve({
3036
+ renderer: new XmlRenderer()
3037
+ });
3038
+ return generateText({
3039
+ abortSignal: config?.abortSignal,
3040
+ providerOptions: this.#options.providerOptions,
3041
+ model: this.#options.model,
3042
+ system: systemPrompt,
3043
+ messages: convertToModelMessages(messages),
3044
+ stopWhen: stepCountIs(25),
3045
+ tools: this.#options.tools,
3046
+ experimental_context: contextVariables,
3047
+ toolChoice: this.#options.toolChoice,
3048
+ onStepFinish: (step) => {
3049
+ const toolCall = step.toolCalls.at(-1);
3050
+ if (toolCall) {
3051
+ console.log(
3052
+ `Debug: ${chalk.yellow("ToolCalled")}: ${toolCall.toolName}(${JSON.stringify(toolCall.input)})`
3053
+ );
3054
+ }
3055
+ }
3056
+ });
1774
3057
  }
1775
3058
  /**
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
3059
+ * Stream a response from the agent.
1786
3060
  *
1787
- * @returns The new branch info (does not switch to it)
1788
- * @throws Error if no messages exist in the conversation
3061
+ * When guardrails are configured, `toUIMessageStream()` is wrapped to provide
3062
+ * self-correction behavior. Direct access to fullStream/textStream bypasses guardrails.
1789
3063
  *
1790
3064
  * @example
1791
- * ```ts
1792
- * // User asked a question, model is generating...
1793
- * context.set(user('What is the weather?'));
1794
- * await context.save();
3065
+ * ```typescript
3066
+ * const stream = await agent.stream({});
1795
3067
  *
1796
- * // User wants to ask something else without waiting
1797
- * const newBranch = await context.btw();
1798
- * // newBranch = { name: 'main-v2', ... }
3068
+ * // With guardrails - use toUIMessageStream for protection
3069
+ * await printer.readableStream(stream.toUIMessageStream());
1799
3070
  *
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();
3071
+ * // Or use printer.stdout which uses toUIMessageStream internally
3072
+ * await printer.stdout(stream);
1804
3073
  * ```
1805
3074
  */
1806
- async btw() {
1807
- await this.#ensureInitialized();
1808
- if (!this.#branch?.headMessageId) {
1809
- throw new Error("Cannot create btw branch: no messages in conversation");
3075
+ async stream(contextVariables, config) {
3076
+ if (!this.#options.context) {
3077
+ throw new Error(`Agent ${this.#options.name} is missing a context.`);
1810
3078
  }
1811
- return this.#createBranchFrom(this.#branch.headMessageId, false);
1812
- }
1813
- /**
1814
- * Update metadata for the current chat.
1815
- *
1816
- * @param updates - Partial metadata to merge (title, metadata)
1817
- *
1818
- * @example
1819
- * ```ts
1820
- * await context.updateChat({
1821
- * title: 'Coding Help Session',
1822
- * metadata: { tags: ['python', 'debugging'] }
1823
- * });
1824
- * ```
1825
- */
1826
- async updateChat(updates) {
1827
- await this.#ensureInitialized();
1828
- const storeUpdates = {};
1829
- if (updates.title !== void 0) {
1830
- storeUpdates.title = updates.title;
3079
+ if (!this.#options.model) {
3080
+ throw new Error(`Agent ${this.#options.name} is missing a model.`);
1831
3081
  }
1832
- if (updates.metadata !== void 0) {
1833
- storeUpdates.metadata = {
1834
- ...this.#chatData?.metadata,
1835
- ...updates.metadata
1836
- };
3082
+ const result = await this.#createRawStream(contextVariables, config);
3083
+ if (this.#guardrails.length === 0) {
3084
+ return result;
1837
3085
  }
1838
- this.#chatData = await this.#store.updateChat(this.#chatId, storeUpdates);
3086
+ return this.#wrapWithGuardrails(result, contextVariables, config);
1839
3087
  }
1840
3088
  /**
1841
- * Consolidate context fragments (no-op for now).
1842
- *
1843
- * This is a placeholder for future functionality that merges context fragments
1844
- * using specific rules. Currently, it does nothing.
1845
- *
1846
- * @experimental
3089
+ * Create a raw stream without guardrail processing.
1847
3090
  */
1848
- consolidate() {
1849
- return void 0;
3091
+ async #createRawStream(contextVariables, config) {
3092
+ const { messages, systemPrompt } = await this.#options.context.resolve({
3093
+ renderer: new XmlRenderer()
3094
+ });
3095
+ const runId = generateId2();
3096
+ return streamText({
3097
+ abortSignal: config?.abortSignal,
3098
+ providerOptions: this.#options.providerOptions,
3099
+ model: this.#options.model,
3100
+ system: systemPrompt,
3101
+ messages: convertToModelMessages(messages),
3102
+ stopWhen: stepCountIs(25),
3103
+ experimental_transform: config?.transform ?? smoothStream(),
3104
+ tools: this.#options.tools,
3105
+ experimental_context: contextVariables,
3106
+ toolChoice: this.#options.toolChoice,
3107
+ onStepFinish: (step) => {
3108
+ const toolCall = step.toolCalls.at(-1);
3109
+ if (toolCall) {
3110
+ console.log(
3111
+ `Debug: (${runId}) ${chalk.bold.yellow("ToolCalled")}: ${toolCall.toolName}(${JSON.stringify(toolCall.input)})`
3112
+ );
3113
+ }
3114
+ }
3115
+ });
1850
3116
  }
1851
3117
  /**
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));
3118
+ * Wrap a StreamTextResult with guardrail protection on toUIMessageStream().
1865
3119
  *
1866
- * // Or write to file for analysis
1867
- * await fs.writeFile('context-debug.json', JSON.stringify(inspection, null, 2));
1868
- * ```
3120
+ * When a guardrail fails:
3121
+ * 1. Accumulated text + feedback is appended as the assistant's self-correction
3122
+ * 2. The feedback is written to the output stream (user sees the correction)
3123
+ * 3. A new stream is started and the model continues from the correction
1869
3124
  */
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
- }
3125
+ #wrapWithGuardrails(result, contextVariables, config) {
3126
+ const maxRetries = config?.maxRetries ?? this.#options.maxGuardrailRetries ?? 3;
3127
+ const context = this.#options.context;
3128
+ const originalToUIMessageStream = result.toUIMessageStream.bind(result);
3129
+ result.toUIMessageStream = (options) => {
3130
+ return createUIMessageStream({
3131
+ generateId: generateId2,
3132
+ execute: async ({ writer }) => {
3133
+ let currentResult = result;
3134
+ let attempt = 0;
3135
+ const guardrailContext = {
3136
+ availableTools: Object.keys(this.tools)
3137
+ };
3138
+ while (attempt < maxRetries) {
3139
+ if (config?.abortSignal?.aborted) {
3140
+ writer.write({ type: "finish" });
3141
+ return;
3142
+ }
3143
+ attempt++;
3144
+ let accumulatedText = "";
3145
+ let guardrailFailed = false;
3146
+ let failureFeedback = "";
3147
+ const uiStream = currentResult === result ? originalToUIMessageStream(options) : currentResult.toUIMessageStream(options);
3148
+ for await (const part of uiStream) {
3149
+ const checkResult = runGuardrailChain(
3150
+ part,
3151
+ this.#guardrails,
3152
+ guardrailContext
3153
+ );
3154
+ if (checkResult.type === "fail") {
3155
+ guardrailFailed = true;
3156
+ failureFeedback = checkResult.feedback;
3157
+ console.log(
3158
+ chalk.yellow(
3159
+ `[${this.#options.name}] Guardrail triggered (attempt ${attempt}/${maxRetries}): ${failureFeedback.slice(0, 50)}...`
3160
+ )
3161
+ );
3162
+ break;
3163
+ }
3164
+ if (checkResult.part.type === "text-delta") {
3165
+ accumulatedText += checkResult.part.delta;
3166
+ }
3167
+ writer.write(checkResult.part);
3168
+ }
3169
+ if (!guardrailFailed) {
3170
+ writer.write({ type: "finish" });
3171
+ return;
3172
+ }
3173
+ if (attempt >= maxRetries) {
3174
+ console.error(
3175
+ chalk.red(
3176
+ `[${this.#options.name}] Guardrail retry limit (${maxRetries}) exceeded.`
3177
+ )
3178
+ );
3179
+ writer.write({ type: "finish" });
3180
+ return;
3181
+ }
3182
+ writer.write({
3183
+ type: "text-delta",
3184
+ id: generateId2(),
3185
+ delta: ` ${failureFeedback}`
3186
+ });
3187
+ const selfCorrectionText = accumulatedText + " " + failureFeedback;
3188
+ context.set(assistantText(selfCorrectionText));
3189
+ await context.save();
3190
+ currentResult = await this.#createRawStream(
3191
+ contextVariables,
3192
+ config
3193
+ );
3194
+ }
3195
+ },
3196
+ onError: (error) => {
3197
+ const message2 = error instanceof Error ? error.message : String(error);
3198
+ return `Stream failed: ${message2}`;
3199
+ }
3200
+ });
1897
3201
  };
3202
+ return result;
3203
+ }
3204
+ clone(overrides) {
3205
+ return new _Agent({
3206
+ ...this.#options,
3207
+ ...overrides
3208
+ });
1898
3209
  }
1899
3210
  };
1900
- function hint(text) {
1901
- return {
1902
- name: "hint",
1903
- data: text
1904
- };
1905
- }
1906
- function fragment(name, ...children) {
1907
- return {
1908
- name,
1909
- data: children
1910
- };
1911
- }
1912
- function role(content) {
1913
- return {
1914
- name: "role",
1915
- data: content
1916
- };
3211
+ function agent(options) {
3212
+ return new Agent(options);
1917
3213
  }
1918
- function user(content) {
1919
- const message2 = typeof content === "string" ? {
1920
- id: generateId(),
1921
- role: "user",
1922
- parts: [{ type: "text", text: content }]
1923
- } : content;
3214
+ function structuredOutput(options) {
1924
3215
  return {
1925
- id: message2.id,
1926
- name: "user",
1927
- data: "content",
1928
- type: "message",
1929
- persist: true,
1930
- codec: {
1931
- decode() {
1932
- return message2;
1933
- },
1934
- encode() {
1935
- return message2;
3216
+ async generate(contextVariables, config) {
3217
+ if (!options.context) {
3218
+ throw new Error(
3219
+ `structuredOutput "${options.name}" is missing a context.`
3220
+ );
1936
3221
  }
1937
- }
1938
- };
1939
- }
1940
- function assistant(message2) {
1941
- return {
1942
- id: message2.id,
1943
- name: "assistant",
1944
- data: "content",
1945
- type: "message",
1946
- persist: true,
1947
- codec: {
1948
- decode() {
1949
- return message2;
1950
- },
1951
- encode() {
1952
- return message2;
3222
+ if (!options.model) {
3223
+ throw new Error(
3224
+ `structuredOutput "${options.name}" is missing a model.`
3225
+ );
1953
3226
  }
1954
- }
1955
- };
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;
3227
+ const { messages, systemPrompt } = await options.context.resolve({
3228
+ renderer: new XmlRenderer()
3229
+ });
3230
+ const result = await generateText({
3231
+ abortSignal: config?.abortSignal,
3232
+ providerOptions: options.providerOptions,
3233
+ model: options.model,
3234
+ system: systemPrompt,
3235
+ messages: convertToModelMessages(messages),
3236
+ stopWhen: stepCountIs(25),
3237
+ experimental_context: contextVariables,
3238
+ experimental_output: Output.object({ schema: options.schema })
3239
+ });
3240
+ return result.experimental_output;
3241
+ },
3242
+ async stream(contextVariables, config) {
3243
+ if (!options.context) {
3244
+ throw new Error(
3245
+ `structuredOutput "${options.name}" is missing a context.`
3246
+ );
1975
3247
  }
3248
+ if (!options.model) {
3249
+ throw new Error(
3250
+ `structuredOutput "${options.name}" is missing a model.`
3251
+ );
3252
+ }
3253
+ const { messages, systemPrompt } = await options.context.resolve({
3254
+ renderer: new XmlRenderer()
3255
+ });
3256
+ return streamText({
3257
+ abortSignal: config?.abortSignal,
3258
+ providerOptions: options.providerOptions,
3259
+ model: options.model,
3260
+ system: systemPrompt,
3261
+ messages: convertToModelMessages(messages),
3262
+ stopWhen: stepCountIs(25),
3263
+ experimental_transform: config?.transform ?? smoothStream(),
3264
+ experimental_context: contextVariables,
3265
+ experimental_output: Output.object({ schema: options.schema })
3266
+ });
1976
3267
  }
1977
3268
  };
1978
3269
  }
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
- }
1987
3270
  export {
3271
+ BinaryInstallError,
3272
+ ComposeStartError,
3273
+ ComposeStrategy,
3274
+ ContainerCreationError,
1988
3275
  ContextEngine,
3276
+ ContextRenderer,
1989
3277
  ContextStore,
3278
+ DockerNotAvailableError,
3279
+ DockerSandboxError,
3280
+ DockerSandboxStrategy,
3281
+ DockerfileBuildError,
3282
+ DockerfileStrategy,
1990
3283
  InMemoryContextStore,
1991
3284
  MarkdownRenderer,
1992
3285
  ModelsRegistry,
3286
+ MountPathError,
3287
+ PackageInstallError,
3288
+ RuntimeStrategy,
1993
3289
  SqliteContextStore,
1994
3290
  TomlRenderer,
1995
3291
  ToonRenderer,
1996
3292
  XmlRenderer,
3293
+ agent,
3294
+ alias,
3295
+ analogy,
1997
3296
  assistant,
1998
3297
  assistantText,
3298
+ clarification,
3299
+ correction,
3300
+ createBinaryBridges,
3301
+ createContainerTool,
3302
+ createDockerSandbox,
1999
3303
  defaultTokenizer,
3304
+ discoverSkillsInDirectory,
2000
3305
  estimate,
3306
+ example,
3307
+ explain,
3308
+ fail,
2001
3309
  fragment,
2002
3310
  getModelsRegistry,
3311
+ glossary,
3312
+ guardrail,
2003
3313
  hint,
3314
+ identity,
3315
+ isComposeOptions,
3316
+ isDockerfileOptions,
3317
+ isFragment,
3318
+ isFragmentObject,
2004
3319
  isMessageFragment,
3320
+ loadSkillMetadata,
2005
3321
  message,
3322
+ parseFrontmatter,
3323
+ pass,
3324
+ persona,
3325
+ preference,
3326
+ quirk,
2006
3327
  role,
3328
+ runGuardrailChain,
3329
+ skills,
3330
+ structuredOutput,
3331
+ styleGuide,
3332
+ term,
3333
+ useSandbox,
2007
3334
  user,
2008
- visualizeGraph
3335
+ userContext,
3336
+ visualizeGraph,
3337
+ workflow
2009
3338
  };
2010
3339
  //# sourceMappingURL=index.js.map