@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.
- package/README.md +114 -119
- package/dist/example-error-recovery.d.ts +2 -0
- package/dist/example-error-recovery.d.ts.map +1 -0
- package/dist/index.d.ts +16 -388
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2391 -1062
- package/dist/index.js.map +4 -4
- package/dist/lib/agent.d.ts +86 -12
- package/dist/lib/agent.d.ts.map +1 -1
- package/dist/lib/engine.d.ts +323 -0
- package/dist/lib/engine.d.ts.map +1 -0
- package/dist/lib/estimate.d.ts +1 -1
- package/dist/lib/estimate.d.ts.map +1 -1
- package/dist/lib/fragments/domain.d.ts +440 -0
- package/dist/lib/fragments/domain.d.ts.map +1 -0
- package/dist/lib/fragments/user.d.ts +122 -0
- package/dist/lib/fragments/user.d.ts.map +1 -0
- package/dist/lib/fragments.d.ts +107 -0
- package/dist/lib/fragments.d.ts.map +1 -0
- package/dist/lib/guardrail.d.ts +138 -0
- package/dist/lib/guardrail.d.ts.map +1 -0
- package/dist/lib/renderers/abstract.renderer.d.ts +1 -1
- package/dist/lib/renderers/abstract.renderer.d.ts.map +1 -1
- package/dist/lib/sandbox/binary-bridges.d.ts +31 -0
- package/dist/lib/sandbox/binary-bridges.d.ts.map +1 -0
- package/dist/lib/sandbox/container-tool.d.ts +134 -0
- package/dist/lib/sandbox/container-tool.d.ts.map +1 -0
- package/dist/lib/sandbox/docker-sandbox.d.ts +471 -0
- package/dist/lib/sandbox/docker-sandbox.d.ts.map +1 -0
- package/dist/lib/sandbox/index.d.ts +4 -0
- package/dist/lib/sandbox/index.d.ts.map +1 -0
- package/dist/lib/skills/fragments.d.ts +24 -0
- package/dist/lib/skills/fragments.d.ts.map +1 -0
- package/dist/lib/skills/index.d.ts +31 -0
- package/dist/lib/skills/index.d.ts.map +1 -0
- package/dist/lib/skills/loader.d.ts +28 -0
- package/dist/lib/skills/loader.d.ts.map +1 -0
- package/dist/lib/skills/types.d.ts +40 -0
- package/dist/lib/skills/types.d.ts.map +1 -0
- package/package.json +7 -3
- package/dist/lib/context.d.ts +0 -56
- 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,
|
|
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 = [...
|
|
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/
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
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
|
-
|
|
910
|
-
|
|
911
|
-
async
|
|
912
|
-
this.#
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
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
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
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
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
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
|
-
|
|
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:
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
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
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
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
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
this.#
|
|
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
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
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:
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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
|
-
|
|
1065
|
-
|
|
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
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
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
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
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
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
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
|
-
|
|
1119
|
-
|
|
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
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
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
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
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
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
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:
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
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
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
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
|
|
1205
|
-
this.#db.prepare("DELETE FROM checkpoints WHERE chatId = ? AND name = ?").run(chatId, name);
|
|
2166
|
+
async configure() {
|
|
1206
2167
|
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
async
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
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
|
-
|
|
1254
|
-
|
|
1255
|
-
async
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
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
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
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
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
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/
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
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
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
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
|
-
|
|
1333
|
-
const
|
|
1334
|
-
|
|
1335
|
-
|
|
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
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
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
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
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
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
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
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
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
|
-
|
|
1388
|
-
|
|
1389
|
-
async
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
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
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
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
|
-
|
|
1415
|
-
|
|
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:
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
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
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
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
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
return
|
|
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
|
-
|
|
1462
|
-
|
|
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:
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
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
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
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
|
|
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
|
-
|
|
1494
|
-
|
|
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
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
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
|
-
|
|
1529
|
-
const decoded = fragment2.codec.decode();
|
|
1530
|
-
messages.push(decoded);
|
|
1531
|
-
}
|
|
1532
|
-
return { systemPrompt, messages };
|
|
2796
|
+
return result;
|
|
1533
2797
|
}
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
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
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
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
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
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
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
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
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
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
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
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
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
2934
|
+
chatId,
|
|
2935
|
+
nodes,
|
|
2936
|
+
branches,
|
|
2937
|
+
checkpoints
|
|
1721
2938
|
};
|
|
1722
2939
|
}
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
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
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
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
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
1788
|
-
*
|
|
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
|
-
* ```
|
|
1792
|
-
*
|
|
1793
|
-
* context.set(user('What is the weather?'));
|
|
1794
|
-
* await context.save();
|
|
3065
|
+
* ```typescript
|
|
3066
|
+
* const stream = await agent.stream({});
|
|
1795
3067
|
*
|
|
1796
|
-
* //
|
|
1797
|
-
*
|
|
1798
|
-
* // newBranch = { name: 'main-v2', ... }
|
|
3068
|
+
* // With guardrails - use toUIMessageStream for protection
|
|
3069
|
+
* await printer.readableStream(stream.toUIMessageStream());
|
|
1799
3070
|
*
|
|
1800
|
-
* //
|
|
1801
|
-
* await
|
|
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
|
|
1807
|
-
|
|
1808
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
...updates.metadata
|
|
1836
|
-
};
|
|
3082
|
+
const result = await this.#createRawStream(contextVariables, config);
|
|
3083
|
+
if (this.#guardrails.length === 0) {
|
|
3084
|
+
return result;
|
|
1837
3085
|
}
|
|
1838
|
-
|
|
3086
|
+
return this.#wrapWithGuardrails(result, contextVariables, config);
|
|
1839
3087
|
}
|
|
1840
3088
|
/**
|
|
1841
|
-
*
|
|
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
|
-
|
|
1849
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
1867
|
-
*
|
|
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
|
-
|
|
1871
|
-
|
|
1872
|
-
const
|
|
1873
|
-
const
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
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
|
-
|
|
3335
|
+
userContext,
|
|
3336
|
+
visualizeGraph,
|
|
3337
|
+
workflow
|
|
2009
3338
|
};
|
|
2010
3339
|
//# sourceMappingURL=index.js.map
|