@deepagents/context 0.13.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1657 -301
- package/dist/index.js.map +4 -4
- package/dist/lib/agent.d.ts.map +1 -1
- package/dist/lib/engine.d.ts +10 -8
- package/dist/lib/engine.d.ts.map +1 -1
- package/dist/lib/fragments.d.ts +1 -0
- package/dist/lib/fragments.d.ts.map +1 -1
- package/dist/lib/guardrail.d.ts +34 -1
- package/dist/lib/guardrail.d.ts.map +1 -1
- package/dist/lib/guardrails/error-recovery.guardrail.d.ts.map +1 -1
- package/dist/lib/skills/fragments.d.ts.map +1 -1
- package/dist/lib/skills/types.d.ts +10 -6
- package/dist/lib/skills/types.d.ts.map +1 -1
- package/dist/lib/store/postgres.store.d.ts +54 -0
- package/dist/lib/store/postgres.store.d.ts.map +1 -0
- package/dist/lib/store/sqlite.store.d.ts +1 -1
- package/dist/lib/store/sqlite.store.d.ts.map +1 -1
- package/dist/lib/store/sqlserver.store.d.ts +54 -0
- package/dist/lib/store/sqlserver.store.d.ts.map +1 -0
- package/dist/lib/store/store.d.ts +2 -1
- package/dist/lib/store/store.d.ts.map +1 -1
- package/package.json +19 -4
package/dist/index.js
CHANGED
|
@@ -247,7 +247,7 @@ function assistantText(content, options) {
|
|
|
247
247
|
parts: [{ type: "text", text: content }]
|
|
248
248
|
});
|
|
249
249
|
}
|
|
250
|
-
var LAZY_ID = Symbol("lazy-id");
|
|
250
|
+
var LAZY_ID = Symbol.for("@deepagents/context:lazy-id");
|
|
251
251
|
function isLazyFragment(fragment2) {
|
|
252
252
|
return LAZY_ID in fragment2;
|
|
253
253
|
}
|
|
@@ -1102,7 +1102,7 @@ var ContextEngine = class {
|
|
|
1102
1102
|
async #createBranchFrom(messageId, switchTo) {
|
|
1103
1103
|
const branches = await this.#store.listBranches(this.#chatId);
|
|
1104
1104
|
const samePrefix = branches.filter(
|
|
1105
|
-
(
|
|
1105
|
+
(it) => it.name === this.#branchName || it.name.startsWith(`${this.#branchName}-v`)
|
|
1106
1106
|
);
|
|
1107
1107
|
const newBranchName = `${this.#branchName}-v${samePrefix.length + 1}`;
|
|
1108
1108
|
const newBranch = {
|
|
@@ -1130,6 +1130,15 @@ var ContextEngine = class {
|
|
|
1130
1130
|
createdAt: newBranch.createdAt
|
|
1131
1131
|
};
|
|
1132
1132
|
}
|
|
1133
|
+
/**
|
|
1134
|
+
* Rewind to a message without clearing pending messages.
|
|
1135
|
+
* Used internally when saving an update to an existing message.
|
|
1136
|
+
*/
|
|
1137
|
+
async #rewindForUpdate(messageId) {
|
|
1138
|
+
const pendingBackup = [...this.#pendingMessages];
|
|
1139
|
+
await this.rewind(messageId);
|
|
1140
|
+
this.#pendingMessages = pendingBackup;
|
|
1141
|
+
}
|
|
1133
1142
|
/**
|
|
1134
1143
|
* Get the current chat ID.
|
|
1135
1144
|
*/
|
|
@@ -1214,7 +1223,18 @@ var ContextEngine = class {
|
|
|
1214
1223
|
messages.push(message(msg.data).codec?.decode());
|
|
1215
1224
|
}
|
|
1216
1225
|
}
|
|
1226
|
+
for (let i = 0; i < this.#pendingMessages.length; i++) {
|
|
1227
|
+
const fragment2 = this.#pendingMessages[i];
|
|
1228
|
+
if (isLazyFragment(fragment2)) {
|
|
1229
|
+
this.#pendingMessages[i] = await this.#resolveLazyFragment(fragment2);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1217
1232
|
for (const fragment2 of this.#pendingMessages) {
|
|
1233
|
+
if (!fragment2.codec) {
|
|
1234
|
+
throw new Error(
|
|
1235
|
+
`Fragment "${fragment2.name}" is missing codec. Lazy fragments must be resolved before decode.`
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1218
1238
|
const decoded = fragment2.codec.decode();
|
|
1219
1239
|
messages.push(decoded);
|
|
1220
1240
|
}
|
|
@@ -1245,9 +1265,24 @@ var ContextEngine = class {
|
|
|
1245
1265
|
this.#pendingMessages[i] = await this.#resolveLazyFragment(fragment2);
|
|
1246
1266
|
}
|
|
1247
1267
|
}
|
|
1268
|
+
for (const fragment2 of this.#pendingMessages) {
|
|
1269
|
+
if (fragment2.id) {
|
|
1270
|
+
const existing = await this.#store.getMessage(fragment2.id);
|
|
1271
|
+
if (existing && existing.parentId) {
|
|
1272
|
+
await this.#rewindForUpdate(existing.parentId);
|
|
1273
|
+
fragment2.id = crypto.randomUUID();
|
|
1274
|
+
break;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1248
1278
|
let parentId = this.#branch.headMessageId;
|
|
1249
1279
|
const now = Date.now();
|
|
1250
1280
|
for (const fragment2 of this.#pendingMessages) {
|
|
1281
|
+
if (!fragment2.codec) {
|
|
1282
|
+
throw new Error(
|
|
1283
|
+
`Fragment "${fragment2.name}" is missing codec. Lazy fragments must be resolved before encode.`
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1251
1286
|
const messageData = {
|
|
1252
1287
|
id: fragment2.id ?? crypto.randomUUID(),
|
|
1253
1288
|
chatId: this.#chatId,
|
|
@@ -1607,33 +1642,28 @@ var ContextEngine = class {
|
|
|
1607
1642
|
return void 0;
|
|
1608
1643
|
}
|
|
1609
1644
|
/**
|
|
1610
|
-
* Extract skill
|
|
1611
|
-
* Returns
|
|
1612
|
-
*
|
|
1613
|
-
* Reads the original `paths` configuration stored in fragment metadata
|
|
1614
|
-
* by the skills() fragment helper.
|
|
1645
|
+
* Extract skill mounts from available_skills fragments.
|
|
1646
|
+
* Returns unified mount array where entries with `name` are individual skills.
|
|
1615
1647
|
*
|
|
1616
1648
|
* @example
|
|
1617
1649
|
* ```ts
|
|
1618
1650
|
* const context = new ContextEngine({ store, chatId, userId })
|
|
1619
1651
|
* .set(skills({ paths: [{ host: './skills', sandbox: '/skills' }] }));
|
|
1620
1652
|
*
|
|
1621
|
-
* const mounts = context.getSkillMounts();
|
|
1622
|
-
* // [{ host: './skills', sandbox: '/skills' }]
|
|
1653
|
+
* const { mounts } = context.getSkillMounts();
|
|
1654
|
+
* // mounts: [{ name: 'bi-dashboards', host: './skills/bi-dashboards/SKILL.md', sandbox: '/skills/bi-dashboards/SKILL.md' }]
|
|
1655
|
+
*
|
|
1656
|
+
* // Extract skills only (entries with name)
|
|
1657
|
+
* const skills = mounts.filter(m => m.name);
|
|
1623
1658
|
* ```
|
|
1624
1659
|
*/
|
|
1625
1660
|
getSkillMounts() {
|
|
1626
|
-
const mounts = [];
|
|
1627
1661
|
for (const fragment2 of this.#fragments) {
|
|
1628
|
-
if (fragment2.name === "available_skills" && fragment2.metadata
|
|
1629
|
-
|
|
1630
|
-
if (typeof mapping === "object" && mapping !== null && typeof mapping.host === "string" && typeof mapping.sandbox === "string") {
|
|
1631
|
-
mounts.push({ host: mapping.host, sandbox: mapping.sandbox });
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1662
|
+
if (fragment2.name === "available_skills" && fragment2.metadata?.mounts) {
|
|
1663
|
+
return { mounts: fragment2.metadata.mounts };
|
|
1634
1664
|
}
|
|
1635
1665
|
}
|
|
1636
|
-
return mounts;
|
|
1666
|
+
return { mounts: [] };
|
|
1637
1667
|
}
|
|
1638
1668
|
/**
|
|
1639
1669
|
* Inspect the full context state for debugging.
|
|
@@ -1870,11 +1900,14 @@ function pass(part) {
|
|
|
1870
1900
|
function fail(feedback) {
|
|
1871
1901
|
return { type: "fail", feedback };
|
|
1872
1902
|
}
|
|
1903
|
+
function stop(part) {
|
|
1904
|
+
return { type: "stop", part };
|
|
1905
|
+
}
|
|
1873
1906
|
function runGuardrailChain(part, guardrails, context) {
|
|
1874
1907
|
let currentPart = part;
|
|
1875
1908
|
for (const guardrail2 of guardrails) {
|
|
1876
1909
|
const result = guardrail2.handle(currentPart, context);
|
|
1877
|
-
if (result.type === "fail") {
|
|
1910
|
+
if (result.type === "fail" || result.type === "stop") {
|
|
1878
1911
|
return result;
|
|
1879
1912
|
}
|
|
1880
1913
|
currentPart = result.part;
|
|
@@ -1920,6 +1953,15 @@ var errorRecoveryGuardrail = {
|
|
|
1920
1953
|
if (errorText.includes("not in request.tools") || errorText.includes("tool") && errorText.includes("not found")) {
|
|
1921
1954
|
const toolMatch = errorText.match(/tool '([^']+)'/);
|
|
1922
1955
|
const toolName = toolMatch ? toolMatch[1] : "unknown";
|
|
1956
|
+
const matchingSkill = context.availableSkills.find(
|
|
1957
|
+
(skill) => skill.name === toolName
|
|
1958
|
+
);
|
|
1959
|
+
if (matchingSkill) {
|
|
1960
|
+
return logAndFail(
|
|
1961
|
+
`Skill confused as tool: ${toolName}`,
|
|
1962
|
+
`"${toolName}" is a skill, not a tool. Read the skill at ${matchingSkill.sandbox} to use it.`
|
|
1963
|
+
);
|
|
1964
|
+
}
|
|
1923
1965
|
if (context.availableTools.length > 0) {
|
|
1924
1966
|
return logAndFail(
|
|
1925
1967
|
`Unregistered tool: ${toolName}`,
|
|
@@ -1937,17 +1979,37 @@ var errorRecoveryGuardrail = {
|
|
|
1937
1979
|
"I generated malformed JSON for the tool arguments. Let me format my tool call properly with valid JSON."
|
|
1938
1980
|
);
|
|
1939
1981
|
}
|
|
1982
|
+
if (errorText.includes("validation failed") && errorText.includes("did not match schema")) {
|
|
1983
|
+
const toolMatch = errorText.match(/parameters for tool (\w+)/);
|
|
1984
|
+
const toolName = toolMatch ? toolMatch[1] : "unknown";
|
|
1985
|
+
const schemaErrors = errorText.match(/errors: \[([^\]]+)\]/)?.[1] || "";
|
|
1986
|
+
return logAndFail(
|
|
1987
|
+
`Schema validation: ${toolName}`,
|
|
1988
|
+
`I called "${toolName}" with invalid parameters. Schema errors: ${schemaErrors}. Let me fix the parameters and try again.`
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1940
1991
|
if (errorText.includes("Parsing failed")) {
|
|
1941
1992
|
return logAndFail(
|
|
1942
1993
|
"Parsing failed",
|
|
1943
1994
|
"My response format was invalid. Let me try again with a properly formatted response."
|
|
1944
1995
|
);
|
|
1945
1996
|
}
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1997
|
+
if (errorText.includes("Failed to call a function") || errorText.includes("failed_generation")) {
|
|
1998
|
+
if (context.availableTools.length > 0) {
|
|
1999
|
+
return logAndFail(
|
|
2000
|
+
"Failed function call",
|
|
2001
|
+
`My function call was malformed. Available tools: ${context.availableTools.join(", ")}. Let me format my tool call correctly.`
|
|
2002
|
+
);
|
|
2003
|
+
}
|
|
2004
|
+
return logAndFail(
|
|
2005
|
+
"Failed function call (no tools)",
|
|
2006
|
+
"My function call was malformed. Let me respond with plain text instead."
|
|
2007
|
+
);
|
|
2008
|
+
}
|
|
2009
|
+
console.log(
|
|
2010
|
+
`${prefix} ${chalk.yellow("Unknown error - stopping without retry")}`
|
|
1950
2011
|
);
|
|
2012
|
+
return stop(part);
|
|
1951
2013
|
}
|
|
1952
2014
|
};
|
|
1953
2015
|
|
|
@@ -2713,6 +2775,9 @@ async function createContainerTool(options = {}) {
|
|
|
2713
2775
|
};
|
|
2714
2776
|
}
|
|
2715
2777
|
|
|
2778
|
+
// packages/context/src/lib/skills/fragments.ts
|
|
2779
|
+
import dedent from "dedent";
|
|
2780
|
+
|
|
2716
2781
|
// packages/context/src/lib/skills/loader.ts
|
|
2717
2782
|
import * as fs from "node:fs";
|
|
2718
2783
|
import * as path2 from "node:path";
|
|
@@ -2793,7 +2858,14 @@ function skills(options) {
|
|
|
2793
2858
|
(s) => !options.exclude.includes(s.name)
|
|
2794
2859
|
);
|
|
2795
2860
|
}
|
|
2796
|
-
|
|
2861
|
+
if (filteredSkills.length === 0) {
|
|
2862
|
+
return {
|
|
2863
|
+
name: "available_skills",
|
|
2864
|
+
data: [],
|
|
2865
|
+
metadata: { mounts: [] }
|
|
2866
|
+
};
|
|
2867
|
+
}
|
|
2868
|
+
const mounts = filteredSkills.map((skill) => {
|
|
2797
2869
|
const originalPath = skill.skillMdPath;
|
|
2798
2870
|
let sandboxPath = originalPath;
|
|
2799
2871
|
for (const [host, sandbox] of pathMapping) {
|
|
@@ -2803,14 +2875,21 @@ function skills(options) {
|
|
|
2803
2875
|
break;
|
|
2804
2876
|
}
|
|
2805
2877
|
}
|
|
2878
|
+
return {
|
|
2879
|
+
name: skill.name,
|
|
2880
|
+
description: skill.description,
|
|
2881
|
+
host: originalPath,
|
|
2882
|
+
sandbox: sandboxPath
|
|
2883
|
+
};
|
|
2884
|
+
});
|
|
2885
|
+
const skillFragments = mounts.map((skill) => {
|
|
2806
2886
|
return {
|
|
2807
2887
|
name: "skill",
|
|
2808
2888
|
data: {
|
|
2809
2889
|
name: skill.name,
|
|
2810
|
-
path:
|
|
2890
|
+
path: skill.sandbox,
|
|
2811
2891
|
description: skill.description
|
|
2812
|
-
}
|
|
2813
|
-
metadata: { originalPath }
|
|
2892
|
+
}
|
|
2814
2893
|
};
|
|
2815
2894
|
});
|
|
2816
2895
|
return {
|
|
@@ -2823,100 +2902,57 @@ function skills(options) {
|
|
|
2823
2902
|
...skillFragments
|
|
2824
2903
|
],
|
|
2825
2904
|
metadata: {
|
|
2826
|
-
|
|
2827
|
-
// Store original path mappings for getSkillMounts()
|
|
2905
|
+
mounts
|
|
2828
2906
|
}
|
|
2829
2907
|
};
|
|
2830
2908
|
}
|
|
2831
|
-
var SKILLS_INSTRUCTIONS = `
|
|
2909
|
+
var SKILLS_INSTRUCTIONS = dedent`A skill is a set of local instructions to follow that is stored in a \`SKILL.md\` file. Below is the list of skills that can be used. Each entry includes a name, description, and file path so you can open the source for full instructions when using a specific skill.
|
|
2832
2910
|
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2911
|
+
### How to use skills
|
|
2912
|
+
- Discovery: The list below shows the skills available in this session (name + description + file path). Skill bodies live on disk at the listed paths.
|
|
2913
|
+
- Trigger rules: If the user names a skill (with \`$SkillName\` or plain text) OR the task clearly matches a skill's description shown below, you must use that skill for that turn before doing anything else. Multiple mentions mean use them all. Do not carry skills across turns unless re-mentioned.
|
|
2914
|
+
- Missing/blocked: If a named skill isn't in the list or the path can't be read, say so briefly and continue with the best fallback.
|
|
2915
|
+
- How to use a skill (progressive disclosure):
|
|
2916
|
+
1) After deciding to use a skill, open its \`SKILL.md\`. Read only enough to follow the workflow.
|
|
2917
|
+
2) If \`SKILL.md\` points to extra folders such as \`references/\`, load only the specific files needed for the request; don't bulk-load everything.
|
|
2918
|
+
3) If \`scripts/\` exist, prefer running or patching them instead of retyping large code blocks.
|
|
2919
|
+
4) If \`assets/\` or templates exist, reuse them instead of recreating from scratch.
|
|
2920
|
+
- Coordination and sequencing:
|
|
2921
|
+
- If multiple skills apply, choose the minimal set that covers the request and state the order you'll use them.
|
|
2922
|
+
- Announce which skill(s) you're using and why (one short line). If you skip an obvious skill, say why.
|
|
2923
|
+
- Context hygiene:
|
|
2924
|
+
- Keep context small: summarize long sections instead of pasting them; only load extra files when needed.
|
|
2925
|
+
- Avoid deep reference-chasing: prefer opening only files directly linked from \`SKILL.md\` unless you're blocked.
|
|
2926
|
+
- When variants exist (frameworks, providers, domains), pick only the relevant reference file(s) and note that choice.
|
|
2927
|
+
- Safety and fallback: If a skill can't be applied cleanly (missing files, unclear instructions), state the issue, pick the next-best approach, and continue.`;
|
|
2839
2928
|
|
|
2840
2929
|
// packages/context/src/lib/store/sqlite.store.ts
|
|
2841
2930
|
import { DatabaseSync } from "node:sqlite";
|
|
2842
|
-
var STORE_DDL = `
|
|
2843
|
-
-- Chats table
|
|
2844
|
-
-- createdAt/updatedAt: DEFAULT for insert, inline SET for updates
|
|
2845
|
-
CREATE TABLE IF NOT EXISTS chats (
|
|
2846
|
-
id TEXT PRIMARY KEY,
|
|
2847
|
-
userId TEXT NOT NULL,
|
|
2848
|
-
title TEXT,
|
|
2849
|
-
metadata TEXT,
|
|
2850
|
-
createdAt INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),
|
|
2851
|
-
updatedAt INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)
|
|
2852
|
-
);
|
|
2853
|
-
|
|
2854
|
-
CREATE INDEX IF NOT EXISTS idx_chats_updatedAt ON chats(updatedAt);
|
|
2855
|
-
CREATE INDEX IF NOT EXISTS idx_chats_userId ON chats(userId);
|
|
2856
|
-
|
|
2857
|
-
-- Messages table (nodes in the DAG)
|
|
2858
|
-
CREATE TABLE IF NOT EXISTS messages (
|
|
2859
|
-
id TEXT PRIMARY KEY,
|
|
2860
|
-
chatId TEXT NOT NULL,
|
|
2861
|
-
parentId TEXT,
|
|
2862
|
-
name TEXT NOT NULL,
|
|
2863
|
-
type TEXT,
|
|
2864
|
-
data TEXT NOT NULL,
|
|
2865
|
-
createdAt INTEGER NOT NULL,
|
|
2866
|
-
FOREIGN KEY (chatId) REFERENCES chats(id) ON DELETE CASCADE,
|
|
2867
|
-
FOREIGN KEY (parentId) REFERENCES messages(id)
|
|
2868
|
-
);
|
|
2869
|
-
|
|
2870
|
-
CREATE INDEX IF NOT EXISTS idx_messages_chatId ON messages(chatId);
|
|
2871
|
-
CREATE INDEX IF NOT EXISTS idx_messages_parentId ON messages(parentId);
|
|
2872
2931
|
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
id TEXT PRIMARY KEY,
|
|
2876
|
-
chatId TEXT NOT NULL,
|
|
2877
|
-
name TEXT NOT NULL,
|
|
2878
|
-
headMessageId TEXT,
|
|
2879
|
-
isActive INTEGER NOT NULL DEFAULT 0,
|
|
2880
|
-
createdAt INTEGER NOT NULL,
|
|
2881
|
-
FOREIGN KEY (chatId) REFERENCES chats(id) ON DELETE CASCADE,
|
|
2882
|
-
FOREIGN KEY (headMessageId) REFERENCES messages(id),
|
|
2883
|
-
UNIQUE(chatId, name)
|
|
2884
|
-
);
|
|
2932
|
+
// packages/context/src/lib/store/ddl.sqlite.sql
|
|
2933
|
+
var ddl_sqlite_default = "-- Context Store DDL for SQLite\n-- This schema implements a DAG-based message history with branching and checkpoints.\n\n-- Performance PRAGMAs (session-level, run on each connection)\nPRAGMA journal_mode = WAL;\nPRAGMA synchronous = NORMAL;\nPRAGMA cache_size = -64000;\nPRAGMA temp_store = MEMORY;\nPRAGMA mmap_size = 268435456;\n\n-- Integrity\nPRAGMA foreign_keys = ON;\n\n-- Chats table\n-- createdAt/updatedAt: DEFAULT for insert, inline SET for updates\nCREATE TABLE IF NOT EXISTS chats (\n id TEXT PRIMARY KEY,\n userId TEXT NOT NULL,\n title TEXT,\n metadata TEXT,\n createdAt INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000),\n updatedAt INTEGER NOT NULL DEFAULT (strftime('%s', 'now') * 1000)\n);\n\nCREATE INDEX IF NOT EXISTS idx_chats_updatedAt ON chats(updatedAt);\nCREATE INDEX IF NOT EXISTS idx_chats_userId ON chats(userId);\n-- Composite index for listChats(): WHERE userId = ? ORDER BY updatedAt DESC\nCREATE INDEX IF NOT EXISTS idx_chats_userId_updatedAt ON chats(userId, updatedAt DESC);\n\n-- Messages table (nodes in the DAG)\nCREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n chatId TEXT NOT NULL,\n parentId TEXT,\n name TEXT NOT NULL,\n type TEXT,\n data TEXT NOT NULL,\n createdAt INTEGER NOT NULL,\n FOREIGN KEY (chatId) REFERENCES chats(id) ON DELETE CASCADE,\n FOREIGN KEY (parentId) REFERENCES messages(id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_messages_chatId ON messages(chatId);\nCREATE INDEX IF NOT EXISTS idx_messages_parentId ON messages(parentId);\n-- Composite index for recursive CTE parent traversal in getMessageChain()\nCREATE INDEX IF NOT EXISTS idx_messages_chatId_parentId ON messages(chatId, parentId);\n\n-- Branches table (pointers to head messages)\nCREATE TABLE IF NOT EXISTS branches (\n id TEXT PRIMARY KEY,\n chatId TEXT NOT NULL,\n name TEXT NOT NULL,\n headMessageId TEXT,\n isActive INTEGER NOT NULL DEFAULT 0,\n createdAt INTEGER NOT NULL,\n FOREIGN KEY (chatId) REFERENCES chats(id) ON DELETE CASCADE,\n FOREIGN KEY (headMessageId) REFERENCES messages(id),\n UNIQUE(chatId, name)\n);\n\nCREATE INDEX IF NOT EXISTS idx_branches_chatId ON branches(chatId);\n-- Composite index for getActiveBranch(): WHERE chatId = ? AND isActive = 1\nCREATE INDEX IF NOT EXISTS idx_branches_chatId_isActive ON branches(chatId, isActive);\n\n-- Checkpoints table (pointers to message nodes)\nCREATE TABLE IF NOT EXISTS checkpoints (\n id TEXT PRIMARY KEY,\n chatId TEXT NOT NULL,\n name TEXT NOT NULL,\n messageId TEXT NOT NULL,\n createdAt INTEGER NOT NULL,\n FOREIGN KEY (chatId) REFERENCES chats(id) ON DELETE CASCADE,\n FOREIGN KEY (messageId) REFERENCES messages(id),\n UNIQUE(chatId, name)\n);\n\nCREATE INDEX IF NOT EXISTS idx_checkpoints_chatId ON checkpoints(chatId);\n\n-- FTS5 virtual table for full-text search\n-- messageId/chatId/name are UNINDEXED (stored but not searchable, used for filtering/joining)\n-- Only 'content' is indexed for full-text search\nCREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(\n messageId UNINDEXED,\n chatId UNINDEXED,\n name UNINDEXED,\n content,\n tokenize='porter unicode61'\n);\n";
|
|
2885
2934
|
|
|
2886
|
-
|
|
2887
|
-
|
|
2888
|
-
-- Checkpoints table (pointers to message nodes)
|
|
2889
|
-
CREATE TABLE IF NOT EXISTS checkpoints (
|
|
2890
|
-
id TEXT PRIMARY KEY,
|
|
2891
|
-
chatId TEXT NOT NULL,
|
|
2892
|
-
name TEXT NOT NULL,
|
|
2893
|
-
messageId TEXT NOT NULL,
|
|
2894
|
-
createdAt INTEGER NOT NULL,
|
|
2895
|
-
FOREIGN KEY (chatId) REFERENCES chats(id) ON DELETE CASCADE,
|
|
2896
|
-
FOREIGN KEY (messageId) REFERENCES messages(id),
|
|
2897
|
-
UNIQUE(chatId, name)
|
|
2898
|
-
);
|
|
2899
|
-
|
|
2900
|
-
CREATE INDEX IF NOT EXISTS idx_checkpoints_chatId ON checkpoints(chatId);
|
|
2901
|
-
|
|
2902
|
-
-- FTS5 virtual table for full-text search
|
|
2903
|
-
-- messageId/chatId/name are UNINDEXED (stored but not searchable, used for filtering/joining)
|
|
2904
|
-
-- Only 'content' is indexed for full-text search
|
|
2905
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5(
|
|
2906
|
-
messageId UNINDEXED,
|
|
2907
|
-
chatId UNINDEXED,
|
|
2908
|
-
name UNINDEXED,
|
|
2909
|
-
content,
|
|
2910
|
-
tokenize='porter unicode61'
|
|
2911
|
-
);
|
|
2912
|
-
`;
|
|
2935
|
+
// packages/context/src/lib/store/sqlite.store.ts
|
|
2913
2936
|
var SqliteContextStore = class extends ContextStore {
|
|
2914
2937
|
#db;
|
|
2938
|
+
#statements = /* @__PURE__ */ new Map();
|
|
2939
|
+
/**
|
|
2940
|
+
* Get or create a prepared statement.
|
|
2941
|
+
* Statements are cached for the lifetime of the store to avoid
|
|
2942
|
+
* repeated SQL parsing and compilation overhead.
|
|
2943
|
+
*/
|
|
2944
|
+
#stmt(sql) {
|
|
2945
|
+
let stmt = this.#statements.get(sql);
|
|
2946
|
+
if (!stmt) {
|
|
2947
|
+
stmt = this.#db.prepare(sql);
|
|
2948
|
+
this.#statements.set(sql, stmt);
|
|
2949
|
+
}
|
|
2950
|
+
return stmt;
|
|
2951
|
+
}
|
|
2915
2952
|
constructor(path3) {
|
|
2916
2953
|
super();
|
|
2917
2954
|
this.#db = new DatabaseSync(path3);
|
|
2918
|
-
this.#db.exec(
|
|
2919
|
-
this.#db.exec(STORE_DDL);
|
|
2955
|
+
this.#db.exec(ddl_sqlite_default);
|
|
2920
2956
|
}
|
|
2921
2957
|
/**
|
|
2922
2958
|
* Execute a function within a transaction.
|
|
@@ -2937,11 +2973,12 @@ var SqliteContextStore = class extends ContextStore {
|
|
|
2937
2973
|
// Chat Operations
|
|
2938
2974
|
// ==========================================================================
|
|
2939
2975
|
async createChat(chat) {
|
|
2940
|
-
this.#useTransaction(() => {
|
|
2941
|
-
this.#db.prepare(
|
|
2976
|
+
return this.#useTransaction(() => {
|
|
2977
|
+
const row = this.#db.prepare(
|
|
2942
2978
|
`INSERT INTO chats (id, userId, title, metadata)
|
|
2943
|
-
VALUES (?, ?, ?, ?)
|
|
2944
|
-
|
|
2979
|
+
VALUES (?, ?, ?, ?)
|
|
2980
|
+
RETURNING *`
|
|
2981
|
+
).get(
|
|
2945
2982
|
chat.id,
|
|
2946
2983
|
chat.userId,
|
|
2947
2984
|
chat.title ?? null,
|
|
@@ -2951,6 +2988,14 @@ var SqliteContextStore = class extends ContextStore {
|
|
|
2951
2988
|
`INSERT INTO branches (id, chatId, name, headMessageId, isActive, createdAt)
|
|
2952
2989
|
VALUES (?, ?, 'main', NULL, 1, ?)`
|
|
2953
2990
|
).run(crypto.randomUUID(), chat.id, Date.now());
|
|
2991
|
+
return {
|
|
2992
|
+
id: row.id,
|
|
2993
|
+
userId: row.userId,
|
|
2994
|
+
title: row.title ?? void 0,
|
|
2995
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
|
|
2996
|
+
createdAt: row.createdAt,
|
|
2997
|
+
updatedAt: row.updatedAt
|
|
2998
|
+
};
|
|
2954
2999
|
});
|
|
2955
3000
|
}
|
|
2956
3001
|
async upsertChat(chat) {
|
|
@@ -3093,21 +3138,16 @@ var SqliteContextStore = class extends ContextStore {
|
|
|
3093
3138
|
// Message Operations (Graph Nodes)
|
|
3094
3139
|
// ==========================================================================
|
|
3095
3140
|
async addMessage(message2) {
|
|
3096
|
-
|
|
3141
|
+
if (message2.parentId === message2.id) {
|
|
3142
|
+
throw new Error(`Message ${message2.id} cannot be its own parent`);
|
|
3143
|
+
}
|
|
3144
|
+
this.#stmt(
|
|
3097
3145
|
`INSERT INTO messages (id, chatId, parentId, name, type, data, createdAt)
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
?5,
|
|
3104
|
-
?6,
|
|
3105
|
-
?7
|
|
3106
|
-
)
|
|
3107
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
3108
|
-
name = excluded.name,
|
|
3109
|
-
type = excluded.type,
|
|
3110
|
-
data = excluded.data`
|
|
3146
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
3147
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
3148
|
+
name = excluded.name,
|
|
3149
|
+
type = excluded.type,
|
|
3150
|
+
data = excluded.data`
|
|
3111
3151
|
).run(
|
|
3112
3152
|
message2.id,
|
|
3113
3153
|
message2.chatId,
|
|
@@ -3118,14 +3158,16 @@ var SqliteContextStore = class extends ContextStore {
|
|
|
3118
3158
|
message2.createdAt
|
|
3119
3159
|
);
|
|
3120
3160
|
const content = typeof message2.data === "string" ? message2.data : JSON.stringify(message2.data);
|
|
3121
|
-
this.#
|
|
3122
|
-
this.#
|
|
3161
|
+
this.#stmt(`DELETE FROM messages_fts WHERE messageId = ?`).run(message2.id);
|
|
3162
|
+
this.#stmt(
|
|
3123
3163
|
`INSERT INTO messages_fts(messageId, chatId, name, content)
|
|
3124
|
-
|
|
3164
|
+
VALUES (?, ?, ?, ?)`
|
|
3125
3165
|
).run(message2.id, message2.chatId, message2.name, content);
|
|
3126
3166
|
}
|
|
3127
3167
|
async getMessage(messageId) {
|
|
3128
|
-
const row = this.#
|
|
3168
|
+
const row = this.#stmt("SELECT * FROM messages WHERE id = ?").get(
|
|
3169
|
+
messageId
|
|
3170
|
+
);
|
|
3129
3171
|
if (!row) {
|
|
3130
3172
|
return void 0;
|
|
3131
3173
|
}
|
|
@@ -3140,15 +3182,16 @@ var SqliteContextStore = class extends ContextStore {
|
|
|
3140
3182
|
};
|
|
3141
3183
|
}
|
|
3142
3184
|
async getMessageChain(headId) {
|
|
3143
|
-
const rows = this.#
|
|
3185
|
+
const rows = this.#stmt(
|
|
3144
3186
|
`WITH RECURSIVE chain AS (
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3151
|
-
|
|
3187
|
+
SELECT *, 0 as depth FROM messages WHERE id = ?
|
|
3188
|
+
UNION ALL
|
|
3189
|
+
SELECT m.*, c.depth + 1 FROM messages m
|
|
3190
|
+
INNER JOIN chain c ON m.id = c.parentId
|
|
3191
|
+
WHERE c.depth < 100000
|
|
3192
|
+
)
|
|
3193
|
+
SELECT * FROM chain
|
|
3194
|
+
ORDER BY depth DESC`
|
|
3152
3195
|
).all(headId);
|
|
3153
3196
|
return rows.map((row) => ({
|
|
3154
3197
|
id: row.id,
|
|
@@ -3161,7 +3204,7 @@ var SqliteContextStore = class extends ContextStore {
|
|
|
3161
3204
|
}));
|
|
3162
3205
|
}
|
|
3163
3206
|
async hasChildren(messageId) {
|
|
3164
|
-
const row = this.#
|
|
3207
|
+
const row = this.#stmt(
|
|
3165
3208
|
"SELECT EXISTS(SELECT 1 FROM messages WHERE parentId = ?) as hasChildren"
|
|
3166
3209
|
).get(messageId);
|
|
3167
3210
|
return row.hasChildren === 1;
|
|
@@ -3208,7 +3251,9 @@ var SqliteContextStore = class extends ContextStore {
|
|
|
3208
3251
|
};
|
|
3209
3252
|
}
|
|
3210
3253
|
async getActiveBranch(chatId) {
|
|
3211
|
-
const row = this.#
|
|
3254
|
+
const row = this.#stmt(
|
|
3255
|
+
"SELECT * FROM branches WHERE chatId = ? AND isActive = 1"
|
|
3256
|
+
).get(chatId);
|
|
3212
3257
|
if (!row) {
|
|
3213
3258
|
return void 0;
|
|
3214
3259
|
}
|
|
@@ -3226,45 +3271,43 @@ var SqliteContextStore = class extends ContextStore {
|
|
|
3226
3271
|
this.#db.prepare("UPDATE branches SET isActive = 1 WHERE id = ?").run(branchId);
|
|
3227
3272
|
}
|
|
3228
3273
|
async updateBranchHead(branchId, messageId) {
|
|
3229
|
-
this.#
|
|
3274
|
+
this.#stmt("UPDATE branches SET headMessageId = ? WHERE id = ?").run(
|
|
3275
|
+
messageId,
|
|
3276
|
+
branchId
|
|
3277
|
+
);
|
|
3230
3278
|
}
|
|
3231
3279
|
async listBranches(chatId) {
|
|
3232
|
-
const
|
|
3280
|
+
const rows = this.#db.prepare(
|
|
3233
3281
|
`SELECT
|
|
3234
3282
|
b.id,
|
|
3235
3283
|
b.name,
|
|
3236
3284
|
b.headMessageId,
|
|
3237
3285
|
b.isActive,
|
|
3238
|
-
b.createdAt
|
|
3286
|
+
b.createdAt,
|
|
3287
|
+
COALESCE(
|
|
3288
|
+
(
|
|
3289
|
+
WITH RECURSIVE chain AS (
|
|
3290
|
+
SELECT id, parentId FROM messages WHERE id = b.headMessageId
|
|
3291
|
+
UNION ALL
|
|
3292
|
+
SELECT m.id, m.parentId FROM messages m
|
|
3293
|
+
INNER JOIN chain c ON m.id = c.parentId
|
|
3294
|
+
)
|
|
3295
|
+
SELECT COUNT(*) FROM chain
|
|
3296
|
+
),
|
|
3297
|
+
0
|
|
3298
|
+
) as messageCount
|
|
3239
3299
|
FROM branches b
|
|
3240
3300
|
WHERE b.chatId = ?
|
|
3241
3301
|
ORDER BY b.createdAt ASC`
|
|
3242
3302
|
).all(chatId);
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
SELECT m.id, m.parentId FROM messages m
|
|
3252
|
-
INNER JOIN chain c ON m.id = c.parentId
|
|
3253
|
-
)
|
|
3254
|
-
SELECT COUNT(*) as count FROM chain`
|
|
3255
|
-
).get(branch.headMessageId);
|
|
3256
|
-
messageCount = countRow.count;
|
|
3257
|
-
}
|
|
3258
|
-
result.push({
|
|
3259
|
-
id: branch.id,
|
|
3260
|
-
name: branch.name,
|
|
3261
|
-
headMessageId: branch.headMessageId,
|
|
3262
|
-
isActive: branch.isActive === 1,
|
|
3263
|
-
messageCount,
|
|
3264
|
-
createdAt: branch.createdAt
|
|
3265
|
-
});
|
|
3266
|
-
}
|
|
3267
|
-
return result;
|
|
3303
|
+
return rows.map((row) => ({
|
|
3304
|
+
id: row.id,
|
|
3305
|
+
name: row.name,
|
|
3306
|
+
headMessageId: row.headMessageId,
|
|
3307
|
+
isActive: row.isActive === 1,
|
|
3308
|
+
messageCount: row.messageCount,
|
|
3309
|
+
createdAt: row.createdAt
|
|
3310
|
+
}));
|
|
3268
3311
|
}
|
|
3269
3312
|
// ==========================================================================
|
|
3270
3313
|
// Checkpoint Operations
|
|
@@ -3417,152 +3460,1450 @@ var InMemoryContextStore = class extends SqliteContextStore {
|
|
|
3417
3460
|
}
|
|
3418
3461
|
};
|
|
3419
3462
|
|
|
3420
|
-
// packages/context/src/lib/
|
|
3421
|
-
|
|
3422
|
-
if (data.nodes.length === 0) {
|
|
3423
|
-
return `[chat: ${data.chatId}]
|
|
3463
|
+
// packages/context/src/lib/store/postgres.store.ts
|
|
3464
|
+
import { createRequire } from "node:module";
|
|
3424
3465
|
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
const childrenByParentId = /* @__PURE__ */ new Map();
|
|
3428
|
-
const branchHeads = /* @__PURE__ */ new Map();
|
|
3429
|
-
const checkpointsByMessageId = /* @__PURE__ */ new Map();
|
|
3430
|
-
for (const node of data.nodes) {
|
|
3431
|
-
const children = childrenByParentId.get(node.parentId) ?? [];
|
|
3432
|
-
children.push(node);
|
|
3433
|
-
childrenByParentId.set(node.parentId, children);
|
|
3434
|
-
}
|
|
3435
|
-
for (const branch of data.branches) {
|
|
3436
|
-
if (branch.headMessageId) {
|
|
3437
|
-
const heads = branchHeads.get(branch.headMessageId) ?? [];
|
|
3438
|
-
heads.push(branch.isActive ? `${branch.name} *` : branch.name);
|
|
3439
|
-
branchHeads.set(branch.headMessageId, heads);
|
|
3440
|
-
}
|
|
3441
|
-
}
|
|
3442
|
-
for (const checkpoint of data.checkpoints) {
|
|
3443
|
-
const cps = checkpointsByMessageId.get(checkpoint.messageId) ?? [];
|
|
3444
|
-
cps.push(checkpoint.name);
|
|
3445
|
-
checkpointsByMessageId.set(checkpoint.messageId, cps);
|
|
3446
|
-
}
|
|
3447
|
-
const roots = childrenByParentId.get(null) ?? [];
|
|
3448
|
-
const lines = [`[chat: ${data.chatId}]`, ""];
|
|
3449
|
-
function renderNode(node, prefix, isLast, isRoot) {
|
|
3450
|
-
const connector = isRoot ? "" : isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
3451
|
-
const contentPreview = node.content.replace(/\n/g, " ");
|
|
3452
|
-
let line = `${prefix}${connector}${node.id.slice(0, 8)} (${node.role}): "${contentPreview}"`;
|
|
3453
|
-
const branches = branchHeads.get(node.id);
|
|
3454
|
-
if (branches) {
|
|
3455
|
-
line += ` <- [${branches.join(", ")}]`;
|
|
3456
|
-
}
|
|
3457
|
-
const checkpoints = checkpointsByMessageId.get(node.id);
|
|
3458
|
-
if (checkpoints) {
|
|
3459
|
-
line += ` {${checkpoints.join(", ")}}`;
|
|
3460
|
-
}
|
|
3461
|
-
lines.push(line);
|
|
3462
|
-
const children = childrenByParentId.get(node.id) ?? [];
|
|
3463
|
-
const childPrefix = isRoot ? "" : prefix + (isLast ? " " : "\u2502 ");
|
|
3464
|
-
for (let i = 0; i < children.length; i++) {
|
|
3465
|
-
renderNode(children[i], childPrefix, i === children.length - 1, false);
|
|
3466
|
-
}
|
|
3467
|
-
}
|
|
3468
|
-
for (let i = 0; i < roots.length; i++) {
|
|
3469
|
-
renderNode(roots[i], "", i === roots.length - 1, true);
|
|
3470
|
-
}
|
|
3471
|
-
lines.push("");
|
|
3472
|
-
lines.push("Legend: * = active branch, {...} = checkpoint");
|
|
3473
|
-
return lines.join("\n");
|
|
3474
|
-
}
|
|
3466
|
+
// packages/context/src/lib/store/ddl.postgres.sql
|
|
3467
|
+
var ddl_postgres_default = "-- Context Store DDL for PostgreSQL\n-- This schema implements a DAG-based message history with branching and checkpoints.\n\n-- Chats table\n-- createdAt/updatedAt: DEFAULT for insert, inline SET for updates\nCREATE TABLE IF NOT EXISTS chats (\n id TEXT PRIMARY KEY,\n userId TEXT NOT NULL,\n title TEXT,\n metadata JSONB,\n createdAt BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT,\n updatedAt BIGINT NOT NULL DEFAULT (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT\n);\n\nCREATE INDEX IF NOT EXISTS idx_chats_updatedAt ON chats(updatedAt);\nCREATE INDEX IF NOT EXISTS idx_chats_userId ON chats(userId);\nCREATE INDEX IF NOT EXISTS idx_chats_metadata ON chats USING GIN (metadata);\n\n-- Messages table (nodes in the DAG)\nCREATE TABLE IF NOT EXISTS messages (\n id TEXT PRIMARY KEY,\n chatId TEXT NOT NULL,\n parentId TEXT,\n name TEXT NOT NULL,\n type TEXT,\n data JSONB NOT NULL,\n createdAt BIGINT NOT NULL,\n FOREIGN KEY (chatId) REFERENCES chats(id) ON DELETE CASCADE,\n FOREIGN KEY (parentId) REFERENCES messages(id)\n);\n\nCREATE INDEX IF NOT EXISTS idx_messages_chatId ON messages(chatId);\nCREATE INDEX IF NOT EXISTS idx_messages_parentId ON messages(parentId);\n\n-- Branches table (pointers to head messages)\nCREATE TABLE IF NOT EXISTS branches (\n id TEXT PRIMARY KEY,\n chatId TEXT NOT NULL,\n name TEXT NOT NULL,\n headMessageId TEXT,\n isActive BOOLEAN NOT NULL DEFAULT FALSE,\n createdAt BIGINT NOT NULL,\n FOREIGN KEY (chatId) REFERENCES chats(id) ON DELETE CASCADE,\n FOREIGN KEY (headMessageId) REFERENCES messages(id),\n UNIQUE(chatId, name)\n);\n\nCREATE INDEX IF NOT EXISTS idx_branches_chatId ON branches(chatId);\n\n-- Checkpoints table (pointers to message nodes)\nCREATE TABLE IF NOT EXISTS checkpoints (\n id TEXT PRIMARY KEY,\n chatId TEXT NOT NULL,\n name TEXT NOT NULL,\n messageId TEXT NOT NULL,\n createdAt BIGINT NOT NULL,\n FOREIGN KEY (chatId) REFERENCES chats(id) ON DELETE CASCADE,\n FOREIGN KEY (messageId) REFERENCES messages(id),\n UNIQUE(chatId, name)\n);\n\nCREATE INDEX IF NOT EXISTS idx_checkpoints_chatId ON checkpoints(chatId);\n\n-- Full-text search using tsvector + GIN index\nCREATE TABLE IF NOT EXISTS messages_fts (\n messageId TEXT PRIMARY KEY REFERENCES messages(id) ON DELETE CASCADE,\n chatId TEXT NOT NULL,\n name TEXT NOT NULL,\n content TEXT NOT NULL,\n content_vector TSVECTOR\n);\n\nCREATE INDEX IF NOT EXISTS idx_messages_fts_vector ON messages_fts USING GIN(content_vector);\nCREATE INDEX IF NOT EXISTS idx_messages_fts_chatId ON messages_fts(chatId);\n\n-- Trigger to automatically update tsvector on insert/update\nCREATE OR REPLACE FUNCTION messages_fts_update_vector() RETURNS TRIGGER AS $$\nBEGIN\n NEW.content_vector := to_tsvector('english', NEW.content);\n RETURN NEW;\nEND;\n$$ LANGUAGE plpgsql;\n\nDROP TRIGGER IF EXISTS messages_fts_vector_update ON messages_fts;\nCREATE TRIGGER messages_fts_vector_update\n BEFORE INSERT OR UPDATE ON messages_fts\n FOR EACH ROW\n EXECUTE FUNCTION messages_fts_update_vector();\n";
|
|
3475
3468
|
|
|
3476
|
-
// packages/context/src/lib/
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
|
|
3480
|
-
Output,
|
|
3481
|
-
convertToModelMessages,
|
|
3482
|
-
createUIMessageStream,
|
|
3483
|
-
generateId as generateId2,
|
|
3484
|
-
generateText,
|
|
3485
|
-
smoothStream,
|
|
3486
|
-
stepCountIs,
|
|
3487
|
-
streamText
|
|
3488
|
-
} from "ai";
|
|
3489
|
-
import chalk2 from "chalk";
|
|
3490
|
-
import "zod";
|
|
3491
|
-
import "@deepagents/agent";
|
|
3492
|
-
var Agent = class _Agent {
|
|
3493
|
-
#options;
|
|
3494
|
-
#guardrails = [];
|
|
3495
|
-
tools;
|
|
3469
|
+
// packages/context/src/lib/store/postgres.store.ts
|
|
3470
|
+
var PostgresContextStore = class _PostgresContextStore extends ContextStore {
|
|
3471
|
+
#pool;
|
|
3472
|
+
#initialized;
|
|
3496
3473
|
constructor(options) {
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
this.#
|
|
3474
|
+
super();
|
|
3475
|
+
const pg = _PostgresContextStore.#requirePg();
|
|
3476
|
+
this.#pool = typeof options.pool === "string" ? new pg.Pool({ connectionString: options.pool }) : new pg.Pool(options.pool);
|
|
3477
|
+
this.#initialized = this.#initialize();
|
|
3500
3478
|
}
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
throw new Error(
|
|
3479
|
+
static #requirePg() {
|
|
3480
|
+
try {
|
|
3481
|
+
const require2 = createRequire(import.meta.url);
|
|
3482
|
+
return require2("pg");
|
|
3483
|
+
} catch {
|
|
3484
|
+
throw new Error(
|
|
3485
|
+
'PostgresContextStore requires the "pg" package. Install it with: npm install pg'
|
|
3486
|
+
);
|
|
3507
3487
|
}
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
return generateText({
|
|
3512
|
-
abortSignal: config?.abortSignal,
|
|
3513
|
-
providerOptions: this.#options.providerOptions,
|
|
3514
|
-
model: this.#options.model,
|
|
3515
|
-
system: systemPrompt,
|
|
3516
|
-
messages: await convertToModelMessages(messages),
|
|
3517
|
-
stopWhen: stepCountIs(25),
|
|
3518
|
-
tools: this.#options.tools,
|
|
3519
|
-
experimental_context: contextVariables,
|
|
3520
|
-
experimental_repairToolCall: repairToolCall,
|
|
3521
|
-
toolChoice: this.#options.toolChoice,
|
|
3522
|
-
onStepFinish: (step) => {
|
|
3523
|
-
const toolCall = step.toolCalls.at(-1);
|
|
3524
|
-
if (toolCall) {
|
|
3525
|
-
console.log(
|
|
3526
|
-
`Debug: ${chalk2.yellow("ToolCalled")}: ${toolCall.toolName}(${JSON.stringify(toolCall.input)})`
|
|
3527
|
-
);
|
|
3528
|
-
}
|
|
3529
|
-
}
|
|
3530
|
-
});
|
|
3488
|
+
}
|
|
3489
|
+
async #initialize() {
|
|
3490
|
+
await this.#pool.query(ddl_postgres_default);
|
|
3531
3491
|
}
|
|
3532
3492
|
/**
|
|
3533
|
-
*
|
|
3534
|
-
*
|
|
3535
|
-
* When guardrails are configured, `toUIMessageStream()` is wrapped to provide
|
|
3536
|
-
* self-correction behavior. Direct access to fullStream/textStream bypasses guardrails.
|
|
3537
|
-
*
|
|
3538
|
-
* @example
|
|
3539
|
-
* ```typescript
|
|
3540
|
-
* const stream = await agent.stream({});
|
|
3541
|
-
*
|
|
3542
|
-
* // With guardrails - use toUIMessageStream for protection
|
|
3543
|
-
* await printer.readableStream(stream.toUIMessageStream());
|
|
3544
|
-
*
|
|
3545
|
-
* // Or use printer.stdout which uses toUIMessageStream internally
|
|
3546
|
-
* await printer.stdout(stream);
|
|
3547
|
-
* ```
|
|
3493
|
+
* Ensure initialization is complete before any operation.
|
|
3548
3494
|
*/
|
|
3549
|
-
async
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3495
|
+
async #ensureInitialized() {
|
|
3496
|
+
await this.#initialized;
|
|
3497
|
+
}
|
|
3498
|
+
/**
|
|
3499
|
+
* Execute a function within a transaction.
|
|
3500
|
+
* Automatically commits on success or rolls back on error.
|
|
3501
|
+
*/
|
|
3502
|
+
async #useTransaction(fn) {
|
|
3503
|
+
await this.#ensureInitialized();
|
|
3504
|
+
const client = await this.#pool.connect();
|
|
3505
|
+
try {
|
|
3506
|
+
await client.query("BEGIN");
|
|
3507
|
+
const result = await fn(client);
|
|
3508
|
+
await client.query("COMMIT");
|
|
3558
3509
|
return result;
|
|
3510
|
+
} catch (error) {
|
|
3511
|
+
await client.query("ROLLBACK");
|
|
3512
|
+
throw error;
|
|
3513
|
+
} finally {
|
|
3514
|
+
client.release();
|
|
3559
3515
|
}
|
|
3560
|
-
return this.#wrapWithGuardrails(result, contextVariables, config);
|
|
3561
3516
|
}
|
|
3562
3517
|
/**
|
|
3563
|
-
*
|
|
3518
|
+
* Execute a query using the pool (no transaction).
|
|
3564
3519
|
*/
|
|
3565
|
-
async #
|
|
3520
|
+
async #query(sql, params) {
|
|
3521
|
+
await this.#ensureInitialized();
|
|
3522
|
+
const result = await this.#pool.query(sql, params);
|
|
3523
|
+
return result.rows;
|
|
3524
|
+
}
|
|
3525
|
+
/**
|
|
3526
|
+
* Close the pool connection.
|
|
3527
|
+
* Call this when done with the store.
|
|
3528
|
+
*/
|
|
3529
|
+
async close() {
|
|
3530
|
+
await this.#pool.end();
|
|
3531
|
+
}
|
|
3532
|
+
// ==========================================================================
|
|
3533
|
+
// Chat Operations
|
|
3534
|
+
// ==========================================================================
|
|
3535
|
+
async createChat(chat) {
|
|
3536
|
+
return this.#useTransaction(async (client) => {
|
|
3537
|
+
const result = await client.query(
|
|
3538
|
+
`INSERT INTO chats (id, userId, title, metadata)
|
|
3539
|
+
VALUES ($1, $2, $3, $4)
|
|
3540
|
+
RETURNING *`,
|
|
3541
|
+
[
|
|
3542
|
+
chat.id,
|
|
3543
|
+
chat.userId,
|
|
3544
|
+
chat.title ?? null,
|
|
3545
|
+
chat.metadata ? JSON.stringify(chat.metadata) : null
|
|
3546
|
+
]
|
|
3547
|
+
);
|
|
3548
|
+
const row = result.rows[0];
|
|
3549
|
+
await client.query(
|
|
3550
|
+
`INSERT INTO branches (id, chatId, name, headMessageId, isActive, createdAt)
|
|
3551
|
+
VALUES ($1, $2, 'main', NULL, TRUE, $3)`,
|
|
3552
|
+
[crypto.randomUUID(), chat.id, Date.now()]
|
|
3553
|
+
);
|
|
3554
|
+
return {
|
|
3555
|
+
id: row.id,
|
|
3556
|
+
userId: row.userid,
|
|
3557
|
+
title: row.title ?? void 0,
|
|
3558
|
+
metadata: row.metadata ?? void 0,
|
|
3559
|
+
createdAt: Number(row.createdat),
|
|
3560
|
+
updatedAt: Number(row.updatedat)
|
|
3561
|
+
};
|
|
3562
|
+
});
|
|
3563
|
+
}
|
|
3564
|
+
async upsertChat(chat) {
|
|
3565
|
+
return this.#useTransaction(async (client) => {
|
|
3566
|
+
const result = await client.query(
|
|
3567
|
+
`INSERT INTO chats (id, userId, title, metadata)
|
|
3568
|
+
VALUES ($1, $2, $3, $4)
|
|
3569
|
+
ON CONFLICT(id) DO UPDATE SET id = EXCLUDED.id
|
|
3570
|
+
RETURNING *`,
|
|
3571
|
+
[
|
|
3572
|
+
chat.id,
|
|
3573
|
+
chat.userId,
|
|
3574
|
+
chat.title ?? null,
|
|
3575
|
+
chat.metadata ? JSON.stringify(chat.metadata) : null
|
|
3576
|
+
]
|
|
3577
|
+
);
|
|
3578
|
+
const row = result.rows[0];
|
|
3579
|
+
await client.query(
|
|
3580
|
+
`INSERT INTO branches (id, chatId, name, headMessageId, isActive, createdAt)
|
|
3581
|
+
VALUES ($1, $2, 'main', NULL, TRUE, $3)
|
|
3582
|
+
ON CONFLICT(chatId, name) DO NOTHING`,
|
|
3583
|
+
[crypto.randomUUID(), chat.id, Date.now()]
|
|
3584
|
+
);
|
|
3585
|
+
return {
|
|
3586
|
+
id: row.id,
|
|
3587
|
+
userId: row.userid,
|
|
3588
|
+
title: row.title ?? void 0,
|
|
3589
|
+
metadata: row.metadata ?? void 0,
|
|
3590
|
+
createdAt: Number(row.createdat),
|
|
3591
|
+
updatedAt: Number(row.updatedat)
|
|
3592
|
+
};
|
|
3593
|
+
});
|
|
3594
|
+
}
|
|
3595
|
+
async getChat(chatId) {
|
|
3596
|
+
const rows = await this.#query("SELECT * FROM chats WHERE id = $1", [chatId]);
|
|
3597
|
+
if (rows.length === 0) {
|
|
3598
|
+
return void 0;
|
|
3599
|
+
}
|
|
3600
|
+
const row = rows[0];
|
|
3601
|
+
return {
|
|
3602
|
+
id: row.id,
|
|
3603
|
+
userId: row.userid,
|
|
3604
|
+
title: row.title ?? void 0,
|
|
3605
|
+
metadata: row.metadata ?? void 0,
|
|
3606
|
+
createdAt: Number(row.createdat),
|
|
3607
|
+
updatedAt: Number(row.updatedat)
|
|
3608
|
+
};
|
|
3609
|
+
}
|
|
3610
|
+
async updateChat(chatId, updates) {
|
|
3611
|
+
const setClauses = [
|
|
3612
|
+
"updatedAt = (EXTRACT(EPOCH FROM NOW()) * 1000)::BIGINT"
|
|
3613
|
+
];
|
|
3614
|
+
const params = [];
|
|
3615
|
+
let paramIndex = 1;
|
|
3616
|
+
if (updates.title !== void 0) {
|
|
3617
|
+
setClauses.push(`title = $${paramIndex++}`);
|
|
3618
|
+
params.push(updates.title ?? null);
|
|
3619
|
+
}
|
|
3620
|
+
if (updates.metadata !== void 0) {
|
|
3621
|
+
setClauses.push(`metadata = $${paramIndex++}`);
|
|
3622
|
+
params.push(JSON.stringify(updates.metadata));
|
|
3623
|
+
}
|
|
3624
|
+
params.push(chatId);
|
|
3625
|
+
const rows = await this.#query(
|
|
3626
|
+
`UPDATE chats SET ${setClauses.join(", ")} WHERE id = $${paramIndex} RETURNING *`,
|
|
3627
|
+
params
|
|
3628
|
+
);
|
|
3629
|
+
const row = rows[0];
|
|
3630
|
+
return {
|
|
3631
|
+
id: row.id,
|
|
3632
|
+
userId: row.userid,
|
|
3633
|
+
title: row.title ?? void 0,
|
|
3634
|
+
metadata: row.metadata ?? void 0,
|
|
3635
|
+
createdAt: Number(row.createdat),
|
|
3636
|
+
updatedAt: Number(row.updatedat)
|
|
3637
|
+
};
|
|
3638
|
+
}
|
|
3639
|
+
async listChats(options) {
|
|
3640
|
+
const params = [];
|
|
3641
|
+
const whereClauses = [];
|
|
3642
|
+
let paramIndex = 1;
|
|
3643
|
+
if (options?.userId) {
|
|
3644
|
+
whereClauses.push(`c.userId = $${paramIndex++}`);
|
|
3645
|
+
params.push(options.userId);
|
|
3646
|
+
}
|
|
3647
|
+
if (options?.metadata) {
|
|
3648
|
+
const keyParam = paramIndex++;
|
|
3649
|
+
const valueParam = paramIndex++;
|
|
3650
|
+
whereClauses.push(`c.metadata->$${keyParam} = $${valueParam}::jsonb`);
|
|
3651
|
+
params.push(options.metadata.key);
|
|
3652
|
+
params.push(JSON.stringify(options.metadata.value));
|
|
3653
|
+
}
|
|
3654
|
+
const whereClause = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
|
|
3655
|
+
let limitClause = "";
|
|
3656
|
+
if (options?.limit !== void 0) {
|
|
3657
|
+
limitClause = ` LIMIT $${paramIndex++}`;
|
|
3658
|
+
params.push(options.limit);
|
|
3659
|
+
if (options.offset !== void 0) {
|
|
3660
|
+
limitClause += ` OFFSET $${paramIndex++}`;
|
|
3661
|
+
params.push(options.offset);
|
|
3662
|
+
}
|
|
3663
|
+
}
|
|
3664
|
+
const rows = await this.#query(
|
|
3665
|
+
`SELECT
|
|
3666
|
+
c.id,
|
|
3667
|
+
c.userId,
|
|
3668
|
+
c.title,
|
|
3669
|
+
c.metadata,
|
|
3670
|
+
c.createdAt,
|
|
3671
|
+
c.updatedAt,
|
|
3672
|
+
COUNT(DISTINCT m.id) as messageCount,
|
|
3673
|
+
COUNT(DISTINCT b.id) as branchCount
|
|
3674
|
+
FROM chats c
|
|
3675
|
+
LEFT JOIN messages m ON m.chatId = c.id
|
|
3676
|
+
LEFT JOIN branches b ON b.chatId = c.id
|
|
3677
|
+
${whereClause}
|
|
3678
|
+
GROUP BY c.id
|
|
3679
|
+
ORDER BY c.updatedAt DESC${limitClause}`,
|
|
3680
|
+
params
|
|
3681
|
+
);
|
|
3682
|
+
return rows.map((row) => ({
|
|
3683
|
+
id: row.id,
|
|
3684
|
+
userId: row.userid,
|
|
3685
|
+
title: row.title ?? void 0,
|
|
3686
|
+
metadata: row.metadata ?? void 0,
|
|
3687
|
+
messageCount: Number(row.messagecount),
|
|
3688
|
+
branchCount: Number(row.branchcount),
|
|
3689
|
+
createdAt: Number(row.createdat),
|
|
3690
|
+
updatedAt: Number(row.updatedat)
|
|
3691
|
+
}));
|
|
3692
|
+
}
|
|
3693
|
+
async deleteChat(chatId, options) {
|
|
3694
|
+
return this.#useTransaction(async (client) => {
|
|
3695
|
+
let sql = "DELETE FROM chats WHERE id = $1";
|
|
3696
|
+
const params = [chatId];
|
|
3697
|
+
if (options?.userId !== void 0) {
|
|
3698
|
+
sql += " AND userId = $2";
|
|
3699
|
+
params.push(options.userId);
|
|
3700
|
+
}
|
|
3701
|
+
const result = await client.query(sql, params);
|
|
3702
|
+
return (result.rowCount ?? 0) > 0;
|
|
3703
|
+
});
|
|
3704
|
+
}
|
|
3705
|
+
// ==========================================================================
|
|
3706
|
+
// Message Operations (Graph Nodes)
|
|
3707
|
+
// ==========================================================================
|
|
3708
|
+
async addMessage(message2) {
|
|
3709
|
+
if (message2.parentId === message2.id) {
|
|
3710
|
+
throw new Error(`Message ${message2.id} cannot be its own parent`);
|
|
3711
|
+
}
|
|
3712
|
+
await this.#useTransaction(async (client) => {
|
|
3713
|
+
await client.query(
|
|
3714
|
+
`INSERT INTO messages (id, chatId, parentId, name, type, data, createdAt)
|
|
3715
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
3716
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
3717
|
+
name = EXCLUDED.name,
|
|
3718
|
+
type = EXCLUDED.type,
|
|
3719
|
+
data = EXCLUDED.data`,
|
|
3720
|
+
[
|
|
3721
|
+
message2.id,
|
|
3722
|
+
message2.chatId,
|
|
3723
|
+
message2.parentId,
|
|
3724
|
+
message2.name,
|
|
3725
|
+
message2.type ?? null,
|
|
3726
|
+
JSON.stringify(message2.data),
|
|
3727
|
+
message2.createdAt
|
|
3728
|
+
]
|
|
3729
|
+
);
|
|
3730
|
+
const content = typeof message2.data === "string" ? message2.data : JSON.stringify(message2.data);
|
|
3731
|
+
await client.query(
|
|
3732
|
+
`INSERT INTO messages_fts (messageId, chatId, name, content)
|
|
3733
|
+
VALUES ($1, $2, $3, $4)
|
|
3734
|
+
ON CONFLICT(messageId) DO UPDATE SET
|
|
3735
|
+
chatId = EXCLUDED.chatId,
|
|
3736
|
+
name = EXCLUDED.name,
|
|
3737
|
+
content = EXCLUDED.content`,
|
|
3738
|
+
[message2.id, message2.chatId, message2.name, content]
|
|
3739
|
+
);
|
|
3740
|
+
});
|
|
3741
|
+
}
|
|
3742
|
+
async getMessage(messageId) {
|
|
3743
|
+
const rows = await this.#query("SELECT * FROM messages WHERE id = $1", [messageId]);
|
|
3744
|
+
if (rows.length === 0) {
|
|
3745
|
+
return void 0;
|
|
3746
|
+
}
|
|
3747
|
+
const row = rows[0];
|
|
3748
|
+
return {
|
|
3749
|
+
id: row.id,
|
|
3750
|
+
chatId: row.chatid,
|
|
3751
|
+
parentId: row.parentid,
|
|
3752
|
+
name: row.name,
|
|
3753
|
+
type: row.type ?? void 0,
|
|
3754
|
+
data: row.data,
|
|
3755
|
+
createdAt: Number(row.createdat)
|
|
3756
|
+
};
|
|
3757
|
+
}
|
|
3758
|
+
async getMessageChain(headId) {
|
|
3759
|
+
const rows = await this.#query(
|
|
3760
|
+
`WITH RECURSIVE chain AS (
|
|
3761
|
+
SELECT *, 0 as depth FROM messages WHERE id = $1
|
|
3762
|
+
UNION ALL
|
|
3763
|
+
SELECT m.*, c.depth + 1 FROM messages m
|
|
3764
|
+
INNER JOIN chain c ON m.id = c.parentId
|
|
3765
|
+
WHERE c.depth < 10000
|
|
3766
|
+
)
|
|
3767
|
+
SELECT * FROM chain
|
|
3768
|
+
ORDER BY depth DESC`,
|
|
3769
|
+
[headId]
|
|
3770
|
+
);
|
|
3771
|
+
return rows.map((row) => ({
|
|
3772
|
+
id: row.id,
|
|
3773
|
+
chatId: row.chatid,
|
|
3774
|
+
parentId: row.parentid,
|
|
3775
|
+
name: row.name,
|
|
3776
|
+
type: row.type ?? void 0,
|
|
3777
|
+
data: row.data,
|
|
3778
|
+
createdAt: Number(row.createdat)
|
|
3779
|
+
}));
|
|
3780
|
+
}
|
|
3781
|
+
async hasChildren(messageId) {
|
|
3782
|
+
const rows = await this.#query(
|
|
3783
|
+
"SELECT EXISTS(SELECT 1 FROM messages WHERE parentId = $1) as exists",
|
|
3784
|
+
[messageId]
|
|
3785
|
+
);
|
|
3786
|
+
return rows[0].exists;
|
|
3787
|
+
}
|
|
3788
|
+
async getMessages(chatId) {
|
|
3789
|
+
const chat = await this.getChat(chatId);
|
|
3790
|
+
if (!chat) {
|
|
3791
|
+
throw new Error(`Chat "${chatId}" not found`);
|
|
3792
|
+
}
|
|
3793
|
+
const activeBranch = await this.getActiveBranch(chatId);
|
|
3794
|
+
if (!activeBranch?.headMessageId) {
|
|
3795
|
+
return [];
|
|
3796
|
+
}
|
|
3797
|
+
return this.getMessageChain(activeBranch.headMessageId);
|
|
3798
|
+
}
|
|
3799
|
+
// ==========================================================================
|
|
3800
|
+
// Branch Operations
|
|
3801
|
+
// ==========================================================================
|
|
3802
|
+
async createBranch(branch) {
|
|
3803
|
+
await this.#query(
|
|
3804
|
+
`INSERT INTO branches (id, chatId, name, headMessageId, isActive, createdAt)
|
|
3805
|
+
VALUES ($1, $2, $3, $4, $5, $6)`,
|
|
3806
|
+
[
|
|
3807
|
+
branch.id,
|
|
3808
|
+
branch.chatId,
|
|
3809
|
+
branch.name,
|
|
3810
|
+
branch.headMessageId,
|
|
3811
|
+
branch.isActive,
|
|
3812
|
+
branch.createdAt
|
|
3813
|
+
]
|
|
3814
|
+
);
|
|
3815
|
+
}
|
|
3816
|
+
async getBranch(chatId, name) {
|
|
3817
|
+
const rows = await this.#query("SELECT * FROM branches WHERE chatId = $1 AND name = $2", [
|
|
3818
|
+
chatId,
|
|
3819
|
+
name
|
|
3820
|
+
]);
|
|
3821
|
+
if (rows.length === 0) {
|
|
3822
|
+
return void 0;
|
|
3823
|
+
}
|
|
3824
|
+
const row = rows[0];
|
|
3825
|
+
return {
|
|
3826
|
+
id: row.id,
|
|
3827
|
+
chatId: row.chatid,
|
|
3828
|
+
name: row.name,
|
|
3829
|
+
headMessageId: row.headmessageid,
|
|
3830
|
+
isActive: row.isactive,
|
|
3831
|
+
createdAt: Number(row.createdat)
|
|
3832
|
+
};
|
|
3833
|
+
}
|
|
3834
|
+
async getActiveBranch(chatId) {
|
|
3835
|
+
const rows = await this.#query("SELECT * FROM branches WHERE chatId = $1 AND isActive = TRUE", [
|
|
3836
|
+
chatId
|
|
3837
|
+
]);
|
|
3838
|
+
if (rows.length === 0) {
|
|
3839
|
+
return void 0;
|
|
3840
|
+
}
|
|
3841
|
+
const row = rows[0];
|
|
3842
|
+
return {
|
|
3843
|
+
id: row.id,
|
|
3844
|
+
chatId: row.chatid,
|
|
3845
|
+
name: row.name,
|
|
3846
|
+
headMessageId: row.headmessageid,
|
|
3847
|
+
isActive: true,
|
|
3848
|
+
createdAt: Number(row.createdat)
|
|
3849
|
+
};
|
|
3850
|
+
}
|
|
3851
|
+
async setActiveBranch(chatId, branchId) {
|
|
3852
|
+
await this.#useTransaction(async (client) => {
|
|
3853
|
+
await client.query(
|
|
3854
|
+
"UPDATE branches SET isActive = FALSE WHERE chatId = $1",
|
|
3855
|
+
[chatId]
|
|
3856
|
+
);
|
|
3857
|
+
await client.query("UPDATE branches SET isActive = TRUE WHERE id = $1", [
|
|
3858
|
+
branchId
|
|
3859
|
+
]);
|
|
3860
|
+
});
|
|
3861
|
+
}
|
|
3862
|
+
async updateBranchHead(branchId, messageId) {
|
|
3863
|
+
await this.#query("UPDATE branches SET headMessageId = $1 WHERE id = $2", [
|
|
3864
|
+
messageId,
|
|
3865
|
+
branchId
|
|
3866
|
+
]);
|
|
3867
|
+
}
|
|
3868
|
+
async listBranches(chatId) {
|
|
3869
|
+
const branches = await this.#query(
|
|
3870
|
+
`SELECT
|
|
3871
|
+
id,
|
|
3872
|
+
name,
|
|
3873
|
+
headMessageId,
|
|
3874
|
+
isActive,
|
|
3875
|
+
createdAt
|
|
3876
|
+
FROM branches
|
|
3877
|
+
WHERE chatId = $1
|
|
3878
|
+
ORDER BY createdAt ASC`,
|
|
3879
|
+
[chatId]
|
|
3880
|
+
);
|
|
3881
|
+
const result = [];
|
|
3882
|
+
for (const branch of branches) {
|
|
3883
|
+
let messageCount = 0;
|
|
3884
|
+
if (branch.headmessageid) {
|
|
3885
|
+
const countRows = await this.#query(
|
|
3886
|
+
`WITH RECURSIVE chain AS (
|
|
3887
|
+
SELECT id, parentId FROM messages WHERE id = $1
|
|
3888
|
+
UNION ALL
|
|
3889
|
+
SELECT m.id, m.parentId FROM messages m
|
|
3890
|
+
INNER JOIN chain c ON m.id = c.parentId
|
|
3891
|
+
)
|
|
3892
|
+
SELECT COUNT(*) as count FROM chain`,
|
|
3893
|
+
[branch.headmessageid]
|
|
3894
|
+
);
|
|
3895
|
+
messageCount = Number(countRows[0].count);
|
|
3896
|
+
}
|
|
3897
|
+
result.push({
|
|
3898
|
+
id: branch.id,
|
|
3899
|
+
name: branch.name,
|
|
3900
|
+
headMessageId: branch.headmessageid,
|
|
3901
|
+
isActive: branch.isactive,
|
|
3902
|
+
messageCount,
|
|
3903
|
+
createdAt: Number(branch.createdat)
|
|
3904
|
+
});
|
|
3905
|
+
}
|
|
3906
|
+
return result;
|
|
3907
|
+
}
|
|
3908
|
+
// ==========================================================================
|
|
3909
|
+
// Checkpoint Operations
|
|
3910
|
+
// ==========================================================================
|
|
3911
|
+
async createCheckpoint(checkpoint) {
|
|
3912
|
+
await this.#query(
|
|
3913
|
+
`INSERT INTO checkpoints (id, chatId, name, messageId, createdAt)
|
|
3914
|
+
VALUES ($1, $2, $3, $4, $5)
|
|
3915
|
+
ON CONFLICT(chatId, name) DO UPDATE SET
|
|
3916
|
+
messageId = EXCLUDED.messageId,
|
|
3917
|
+
createdAt = EXCLUDED.createdAt`,
|
|
3918
|
+
[
|
|
3919
|
+
checkpoint.id,
|
|
3920
|
+
checkpoint.chatId,
|
|
3921
|
+
checkpoint.name,
|
|
3922
|
+
checkpoint.messageId,
|
|
3923
|
+
checkpoint.createdAt
|
|
3924
|
+
]
|
|
3925
|
+
);
|
|
3926
|
+
}
|
|
3927
|
+
async getCheckpoint(chatId, name) {
|
|
3928
|
+
const rows = await this.#query("SELECT * FROM checkpoints WHERE chatId = $1 AND name = $2", [
|
|
3929
|
+
chatId,
|
|
3930
|
+
name
|
|
3931
|
+
]);
|
|
3932
|
+
if (rows.length === 0) {
|
|
3933
|
+
return void 0;
|
|
3934
|
+
}
|
|
3935
|
+
const row = rows[0];
|
|
3936
|
+
return {
|
|
3937
|
+
id: row.id,
|
|
3938
|
+
chatId: row.chatid,
|
|
3939
|
+
name: row.name,
|
|
3940
|
+
messageId: row.messageid,
|
|
3941
|
+
createdAt: Number(row.createdat)
|
|
3942
|
+
};
|
|
3943
|
+
}
|
|
3944
|
+
async listCheckpoints(chatId) {
|
|
3945
|
+
const rows = await this.#query(
|
|
3946
|
+
`SELECT id, name, messageId, createdAt
|
|
3947
|
+
FROM checkpoints
|
|
3948
|
+
WHERE chatId = $1
|
|
3949
|
+
ORDER BY createdAt DESC`,
|
|
3950
|
+
[chatId]
|
|
3951
|
+
);
|
|
3952
|
+
return rows.map((row) => ({
|
|
3953
|
+
id: row.id,
|
|
3954
|
+
name: row.name,
|
|
3955
|
+
messageId: row.messageid,
|
|
3956
|
+
createdAt: Number(row.createdat)
|
|
3957
|
+
}));
|
|
3958
|
+
}
|
|
3959
|
+
async deleteCheckpoint(chatId, name) {
|
|
3960
|
+
await this.#query(
|
|
3961
|
+
"DELETE FROM checkpoints WHERE chatId = $1 AND name = $2",
|
|
3962
|
+
[chatId, name]
|
|
3963
|
+
);
|
|
3964
|
+
}
|
|
3965
|
+
// ==========================================================================
|
|
3966
|
+
// Search Operations
|
|
3967
|
+
// ==========================================================================
|
|
3968
|
+
async searchMessages(chatId, query, options) {
|
|
3969
|
+
const limit = options?.limit ?? 20;
|
|
3970
|
+
const roles = options?.roles;
|
|
3971
|
+
let sql = `
|
|
3972
|
+
SELECT
|
|
3973
|
+
m.id,
|
|
3974
|
+
m.chatId,
|
|
3975
|
+
m.parentId,
|
|
3976
|
+
m.name,
|
|
3977
|
+
m.type,
|
|
3978
|
+
m.data,
|
|
3979
|
+
m.createdAt,
|
|
3980
|
+
ts_rank(fts.content_vector, plainto_tsquery('english', $2)) as rank,
|
|
3981
|
+
ts_headline('english', fts.content, plainto_tsquery('english', $2),
|
|
3982
|
+
'StartSel=<mark>, StopSel=</mark>, MaxWords=32, MinWords=5, MaxFragments=1') as snippet
|
|
3983
|
+
FROM messages_fts fts
|
|
3984
|
+
JOIN messages m ON m.id = fts.messageId
|
|
3985
|
+
WHERE fts.content_vector @@ plainto_tsquery('english', $2)
|
|
3986
|
+
AND fts.chatId = $1
|
|
3987
|
+
`;
|
|
3988
|
+
const params = [chatId, query];
|
|
3989
|
+
let paramIndex = 3;
|
|
3990
|
+
if (roles && roles.length > 0) {
|
|
3991
|
+
const placeholders = roles.map(() => `$${paramIndex++}`).join(", ");
|
|
3992
|
+
sql += ` AND fts.name IN (${placeholders})`;
|
|
3993
|
+
params.push(...roles);
|
|
3994
|
+
}
|
|
3995
|
+
sql += ` ORDER BY rank DESC LIMIT $${paramIndex}`;
|
|
3996
|
+
params.push(limit);
|
|
3997
|
+
const rows = await this.#query(sql, params);
|
|
3998
|
+
return rows.map((row) => ({
|
|
3999
|
+
message: {
|
|
4000
|
+
id: row.id,
|
|
4001
|
+
chatId: row.chatid,
|
|
4002
|
+
parentId: row.parentid,
|
|
4003
|
+
name: row.name,
|
|
4004
|
+
type: row.type ?? void 0,
|
|
4005
|
+
data: row.data,
|
|
4006
|
+
createdAt: Number(row.createdat)
|
|
4007
|
+
},
|
|
4008
|
+
rank: row.rank,
|
|
4009
|
+
snippet: row.snippet
|
|
4010
|
+
}));
|
|
4011
|
+
}
|
|
4012
|
+
// ==========================================================================
|
|
4013
|
+
// Visualization Operations
|
|
4014
|
+
// ==========================================================================
|
|
4015
|
+
async getGraph(chatId) {
|
|
4016
|
+
const messageRows = await this.#query(
|
|
4017
|
+
`SELECT id, parentId, name, data, createdAt
|
|
4018
|
+
FROM messages
|
|
4019
|
+
WHERE chatId = $1
|
|
4020
|
+
ORDER BY createdAt ASC`,
|
|
4021
|
+
[chatId]
|
|
4022
|
+
);
|
|
4023
|
+
const nodes = messageRows.map((row) => {
|
|
4024
|
+
const data = row.data;
|
|
4025
|
+
const content = typeof data === "string" ? data : JSON.stringify(data);
|
|
4026
|
+
return {
|
|
4027
|
+
id: row.id,
|
|
4028
|
+
parentId: row.parentid,
|
|
4029
|
+
role: row.name,
|
|
4030
|
+
content: content.length > 50 ? content.slice(0, 50) + "..." : content,
|
|
4031
|
+
createdAt: Number(row.createdat)
|
|
4032
|
+
};
|
|
4033
|
+
});
|
|
4034
|
+
const branchRows = await this.#query(
|
|
4035
|
+
`SELECT name, headMessageId, isActive
|
|
4036
|
+
FROM branches
|
|
4037
|
+
WHERE chatId = $1
|
|
4038
|
+
ORDER BY createdAt ASC`,
|
|
4039
|
+
[chatId]
|
|
4040
|
+
);
|
|
4041
|
+
const branches = branchRows.map((row) => ({
|
|
4042
|
+
name: row.name,
|
|
4043
|
+
headMessageId: row.headmessageid,
|
|
4044
|
+
isActive: row.isactive
|
|
4045
|
+
}));
|
|
4046
|
+
const checkpointRows = await this.#query(
|
|
4047
|
+
`SELECT name, messageId
|
|
4048
|
+
FROM checkpoints
|
|
4049
|
+
WHERE chatId = $1
|
|
4050
|
+
ORDER BY createdAt ASC`,
|
|
4051
|
+
[chatId]
|
|
4052
|
+
);
|
|
4053
|
+
const checkpoints = checkpointRows.map((row) => ({
|
|
4054
|
+
name: row.name,
|
|
4055
|
+
messageId: row.messageid
|
|
4056
|
+
}));
|
|
4057
|
+
return {
|
|
4058
|
+
chatId,
|
|
4059
|
+
nodes,
|
|
4060
|
+
branches,
|
|
4061
|
+
checkpoints
|
|
4062
|
+
};
|
|
4063
|
+
}
|
|
4064
|
+
};
|
|
4065
|
+
|
|
4066
|
+
// packages/context/src/lib/store/sqlserver.store.ts
|
|
4067
|
+
import { createRequire as createRequire2 } from "node:module";
|
|
4068
|
+
|
|
4069
|
+
// packages/context/src/lib/store/ddl.sqlserver.sql
|
|
4070
|
+
var ddl_sqlserver_default = "-- Context Store DDL for SQL Server\n-- This schema implements a DAG-based message history with branching and checkpoints.\n\n-- Chats table\n-- createdAt/updatedAt: DEFAULT for insert, inline SET for updates\nIF OBJECT_ID('chats', 'U') IS NULL\nBEGIN\n CREATE TABLE chats (\n id NVARCHAR(255) PRIMARY KEY,\n userId NVARCHAR(255) NOT NULL,\n title NVARCHAR(MAX),\n metadata NVARCHAR(MAX),\n createdAt BIGINT NOT NULL DEFAULT DATEDIFF_BIG(ms, '1970-01-01', GETUTCDATE()),\n updatedAt BIGINT NOT NULL DEFAULT DATEDIFF_BIG(ms, '1970-01-01', GETUTCDATE())\n );\nEND;\n\nIF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_chats_updatedAt' AND object_id = OBJECT_ID('chats'))\n CREATE INDEX idx_chats_updatedAt ON chats(updatedAt);\n\nIF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_chats_userId' AND object_id = OBJECT_ID('chats'))\n CREATE INDEX idx_chats_userId ON chats(userId);\n\n-- Messages table (nodes in the DAG)\nIF OBJECT_ID('messages', 'U') IS NULL\nBEGIN\n CREATE TABLE messages (\n id NVARCHAR(255) PRIMARY KEY,\n chatId NVARCHAR(255) NOT NULL,\n parentId NVARCHAR(255),\n name NVARCHAR(255) NOT NULL,\n type NVARCHAR(255),\n data NVARCHAR(MAX) NOT NULL,\n createdAt BIGINT NOT NULL,\n FOREIGN KEY (chatId) REFERENCES chats(id) ON DELETE CASCADE,\n FOREIGN KEY (parentId) REFERENCES messages(id)\n );\nEND;\n\nIF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_messages_chatId' AND object_id = OBJECT_ID('messages'))\n CREATE INDEX idx_messages_chatId ON messages(chatId);\n\nIF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_messages_parentId' AND object_id = OBJECT_ID('messages'))\n CREATE INDEX idx_messages_parentId ON messages(parentId);\n\nIF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_messages_chatId_parentId' AND object_id = OBJECT_ID('messages'))\n CREATE INDEX idx_messages_chatId_parentId ON messages(chatId, parentId);\n\n-- Branches table (pointers to head messages)\nIF OBJECT_ID('branches', 'U') IS NULL\nBEGIN\n CREATE TABLE branches (\n id NVARCHAR(255) PRIMARY KEY,\n chatId NVARCHAR(255) NOT NULL,\n name NVARCHAR(255) NOT NULL,\n headMessageId NVARCHAR(255),\n isActive BIT NOT NULL DEFAULT 0,\n createdAt BIGINT NOT NULL,\n FOREIGN KEY (chatId) REFERENCES chats(id) ON DELETE CASCADE,\n FOREIGN KEY (headMessageId) REFERENCES messages(id),\n CONSTRAINT UQ_branches_chatId_name UNIQUE(chatId, name)\n );\nEND;\n\nIF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_branches_chatId' AND object_id = OBJECT_ID('branches'))\n CREATE INDEX idx_branches_chatId ON branches(chatId);\n\nIF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_branches_chatId_isActive' AND object_id = OBJECT_ID('branches'))\n CREATE INDEX idx_branches_chatId_isActive ON branches(chatId, isActive);\n\n-- Checkpoints table (pointers to message nodes)\nIF OBJECT_ID('checkpoints', 'U') IS NULL\nBEGIN\n CREATE TABLE checkpoints (\n id NVARCHAR(255) PRIMARY KEY,\n chatId NVARCHAR(255) NOT NULL,\n name NVARCHAR(255) NOT NULL,\n messageId NVARCHAR(255) NOT NULL,\n createdAt BIGINT NOT NULL,\n FOREIGN KEY (chatId) REFERENCES chats(id) ON DELETE CASCADE,\n FOREIGN KEY (messageId) REFERENCES messages(id),\n CONSTRAINT UQ_checkpoints_chatId_name UNIQUE(chatId, name)\n );\nEND;\n\nIF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_checkpoints_chatId' AND object_id = OBJECT_ID('checkpoints'))\n CREATE INDEX idx_checkpoints_chatId ON checkpoints(chatId);\n\n-- Full-text search table\nIF OBJECT_ID('messages_fts', 'U') IS NULL\nBEGIN\n CREATE TABLE messages_fts (\n messageId NVARCHAR(255) NOT NULL,\n chatId NVARCHAR(255) NOT NULL,\n name NVARCHAR(255) NOT NULL,\n content NVARCHAR(MAX) NOT NULL,\n CONSTRAINT PK_messages_fts PRIMARY KEY (messageId),\n FOREIGN KEY (messageId) REFERENCES messages(id) ON DELETE CASCADE\n );\nEND;\n\nIF NOT EXISTS (SELECT * FROM sys.indexes WHERE name = 'idx_messages_fts_chatId' AND object_id = OBJECT_ID('messages_fts'))\n CREATE INDEX idx_messages_fts_chatId ON messages_fts(chatId);\n\n-- Full-text catalog and index (only if FTS is installed)\n-- FTS is optional - search will gracefully degrade without it\nIF SERVERPROPERTY('IsFullTextInstalled') = 1\nBEGIN\n -- Create catalog if not exists\n IF NOT EXISTS (SELECT * FROM sys.fulltext_catalogs WHERE name = 'context_store_catalog')\n CREATE FULLTEXT CATALOG context_store_catalog AS DEFAULT;\n\n -- Create full-text index on messages_fts.content\n -- Note: This requires the table to have a unique index, which PK provides\n IF NOT EXISTS (SELECT * FROM sys.fulltext_indexes WHERE object_id = OBJECT_ID('messages_fts'))\n BEGIN\n CREATE FULLTEXT INDEX ON messages_fts(content)\n KEY INDEX PK_messages_fts\n ON context_store_catalog\n WITH STOPLIST = SYSTEM;\n END;\nEND;\n";
|
|
4071
|
+
|
|
4072
|
+
// packages/context/src/lib/store/sqlserver.store.ts
|
|
4073
|
+
var SqlServerContextStore = class _SqlServerContextStore extends ContextStore {
|
|
4074
|
+
#pool;
|
|
4075
|
+
#initialized;
|
|
4076
|
+
constructor(options) {
|
|
4077
|
+
super();
|
|
4078
|
+
const mssql = _SqlServerContextStore.#requireMssql();
|
|
4079
|
+
this.#pool = typeof options.pool === "string" ? new mssql.ConnectionPool(options.pool) : new mssql.ConnectionPool(options.pool);
|
|
4080
|
+
this.#initialized = this.#initialize();
|
|
4081
|
+
}
|
|
4082
|
+
static #requireMssql() {
|
|
4083
|
+
try {
|
|
4084
|
+
const require2 = createRequire2(import.meta.url);
|
|
4085
|
+
return require2("mssql");
|
|
4086
|
+
} catch {
|
|
4087
|
+
throw new Error(
|
|
4088
|
+
'SqlServerContextStore requires the "mssql" package. Install it with: npm install mssql'
|
|
4089
|
+
);
|
|
4090
|
+
}
|
|
4091
|
+
}
|
|
4092
|
+
async #initialize() {
|
|
4093
|
+
await this.#pool.connect();
|
|
4094
|
+
const batches = ddl_sqlserver_default.split(/\bGO\b/i).filter((b) => b.trim());
|
|
4095
|
+
for (const batch of batches) {
|
|
4096
|
+
if (batch.trim()) {
|
|
4097
|
+
await this.#pool.request().batch(batch);
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
}
|
|
4101
|
+
/**
|
|
4102
|
+
* Ensure initialization is complete before any operation.
|
|
4103
|
+
*/
|
|
4104
|
+
async #ensureInitialized() {
|
|
4105
|
+
await this.#initialized;
|
|
4106
|
+
}
|
|
4107
|
+
/**
|
|
4108
|
+
* Execute a function within a transaction.
|
|
4109
|
+
* Automatically commits on success or rolls back on error.
|
|
4110
|
+
*/
|
|
4111
|
+
async #useTransaction(fn) {
|
|
4112
|
+
await this.#ensureInitialized();
|
|
4113
|
+
const mssql = _SqlServerContextStore.#requireMssql();
|
|
4114
|
+
const transaction = new mssql.Transaction(this.#pool);
|
|
4115
|
+
try {
|
|
4116
|
+
await transaction.begin();
|
|
4117
|
+
const result = await fn(transaction);
|
|
4118
|
+
await transaction.commit();
|
|
4119
|
+
return result;
|
|
4120
|
+
} catch (error) {
|
|
4121
|
+
await transaction.rollback();
|
|
4122
|
+
throw error;
|
|
4123
|
+
}
|
|
4124
|
+
}
|
|
4125
|
+
/**
|
|
4126
|
+
* Execute a query using the pool (no transaction).
|
|
4127
|
+
* Converts positional params to SQL Server named params (@p0, @p1, ...).
|
|
4128
|
+
*/
|
|
4129
|
+
async #query(sql, params) {
|
|
4130
|
+
await this.#ensureInitialized();
|
|
4131
|
+
const request = this.#pool.request();
|
|
4132
|
+
params?.forEach((value, index) => {
|
|
4133
|
+
request.input(`p${index}`, value);
|
|
4134
|
+
});
|
|
4135
|
+
const result = await request.query(sql);
|
|
4136
|
+
return result.recordset;
|
|
4137
|
+
}
|
|
4138
|
+
/**
|
|
4139
|
+
* Close the pool connection.
|
|
4140
|
+
* Call this when done with the store.
|
|
4141
|
+
*/
|
|
4142
|
+
async close() {
|
|
4143
|
+
await this.#pool.close();
|
|
4144
|
+
}
|
|
4145
|
+
// ==========================================================================
|
|
4146
|
+
// Chat Operations
|
|
4147
|
+
// ==========================================================================
|
|
4148
|
+
async createChat(chat) {
|
|
4149
|
+
return this.#useTransaction(async (transaction) => {
|
|
4150
|
+
const mssql = _SqlServerContextStore.#requireMssql();
|
|
4151
|
+
const request = transaction.request();
|
|
4152
|
+
request.input("p0", mssql.NVarChar, chat.id);
|
|
4153
|
+
request.input("p1", mssql.NVarChar, chat.userId);
|
|
4154
|
+
request.input("p2", mssql.NVarChar, chat.title ?? null);
|
|
4155
|
+
request.input(
|
|
4156
|
+
"p3",
|
|
4157
|
+
mssql.NVarChar,
|
|
4158
|
+
chat.metadata ? JSON.stringify(chat.metadata) : null
|
|
4159
|
+
);
|
|
4160
|
+
const result = await request.query(`
|
|
4161
|
+
INSERT INTO chats (id, userId, title, metadata)
|
|
4162
|
+
OUTPUT INSERTED.*
|
|
4163
|
+
VALUES (@p0, @p1, @p2, @p3)
|
|
4164
|
+
`);
|
|
4165
|
+
const row = result.recordset[0];
|
|
4166
|
+
const branchRequest = transaction.request();
|
|
4167
|
+
branchRequest.input("p0", mssql.NVarChar, crypto.randomUUID());
|
|
4168
|
+
branchRequest.input("p1", mssql.NVarChar, chat.id);
|
|
4169
|
+
branchRequest.input("p2", mssql.BigInt, Date.now());
|
|
4170
|
+
await branchRequest.query(`
|
|
4171
|
+
INSERT INTO branches (id, chatId, name, headMessageId, isActive, createdAt)
|
|
4172
|
+
VALUES (@p0, @p1, 'main', NULL, 1, @p2)
|
|
4173
|
+
`);
|
|
4174
|
+
return {
|
|
4175
|
+
id: row.id,
|
|
4176
|
+
userId: row.userId,
|
|
4177
|
+
title: row.title ?? void 0,
|
|
4178
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
|
|
4179
|
+
createdAt: Number(row.createdAt),
|
|
4180
|
+
updatedAt: Number(row.updatedAt)
|
|
4181
|
+
};
|
|
4182
|
+
});
|
|
4183
|
+
}
|
|
4184
|
+
async upsertChat(chat) {
|
|
4185
|
+
return this.#useTransaction(async (transaction) => {
|
|
4186
|
+
const mssql = _SqlServerContextStore.#requireMssql();
|
|
4187
|
+
const request = transaction.request();
|
|
4188
|
+
request.input("p0", mssql.NVarChar, chat.id);
|
|
4189
|
+
request.input("p1", mssql.NVarChar, chat.userId);
|
|
4190
|
+
request.input("p2", mssql.NVarChar, chat.title ?? null);
|
|
4191
|
+
request.input(
|
|
4192
|
+
"p3",
|
|
4193
|
+
mssql.NVarChar,
|
|
4194
|
+
chat.metadata ? JSON.stringify(chat.metadata) : null
|
|
4195
|
+
);
|
|
4196
|
+
request.input("p4", mssql.BigInt, BigInt(Date.now()));
|
|
4197
|
+
const result = await request.query(`
|
|
4198
|
+
MERGE chats AS target
|
|
4199
|
+
USING (SELECT @p0 AS id, @p1 AS userId, @p2 AS title, @p3 AS metadata) AS source
|
|
4200
|
+
ON target.id = source.id
|
|
4201
|
+
WHEN MATCHED THEN
|
|
4202
|
+
UPDATE SET id = target.id
|
|
4203
|
+
WHEN NOT MATCHED THEN
|
|
4204
|
+
INSERT (id, userId, title, metadata, createdAt, updatedAt)
|
|
4205
|
+
VALUES (source.id, source.userId, source.title, source.metadata, @p4, @p4)
|
|
4206
|
+
OUTPUT INSERTED.*;
|
|
4207
|
+
`);
|
|
4208
|
+
const row = result.recordset[0];
|
|
4209
|
+
const branchRequest = transaction.request();
|
|
4210
|
+
branchRequest.input("p0", mssql.NVarChar, crypto.randomUUID());
|
|
4211
|
+
branchRequest.input("p1", mssql.NVarChar, chat.id);
|
|
4212
|
+
branchRequest.input("p2", mssql.BigInt, Date.now());
|
|
4213
|
+
await branchRequest.query(`
|
|
4214
|
+
IF NOT EXISTS (SELECT 1 FROM branches WHERE chatId = @p1 AND name = 'main')
|
|
4215
|
+
BEGIN
|
|
4216
|
+
INSERT INTO branches (id, chatId, name, headMessageId, isActive, createdAt)
|
|
4217
|
+
VALUES (@p0, @p1, 'main', NULL, 1, @p2)
|
|
4218
|
+
END
|
|
4219
|
+
`);
|
|
4220
|
+
return {
|
|
4221
|
+
id: row.id,
|
|
4222
|
+
userId: row.userId,
|
|
4223
|
+
title: row.title ?? void 0,
|
|
4224
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
|
|
4225
|
+
createdAt: Number(row.createdAt),
|
|
4226
|
+
updatedAt: Number(row.updatedAt)
|
|
4227
|
+
};
|
|
4228
|
+
});
|
|
4229
|
+
}
|
|
4230
|
+
async getChat(chatId) {
|
|
4231
|
+
const rows = await this.#query("SELECT * FROM chats WHERE id = @p0", [chatId]);
|
|
4232
|
+
if (rows.length === 0) {
|
|
4233
|
+
return void 0;
|
|
4234
|
+
}
|
|
4235
|
+
const row = rows[0];
|
|
4236
|
+
return {
|
|
4237
|
+
id: row.id,
|
|
4238
|
+
userId: row.userId,
|
|
4239
|
+
title: row.title ?? void 0,
|
|
4240
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
|
|
4241
|
+
createdAt: Number(row.createdAt),
|
|
4242
|
+
updatedAt: Number(row.updatedAt)
|
|
4243
|
+
};
|
|
4244
|
+
}
|
|
4245
|
+
async updateChat(chatId, updates) {
|
|
4246
|
+
const setClauses = [
|
|
4247
|
+
"updatedAt = DATEDIFF_BIG(ms, '1970-01-01', GETUTCDATE())"
|
|
4248
|
+
];
|
|
4249
|
+
const params = [];
|
|
4250
|
+
let paramIndex = 0;
|
|
4251
|
+
if (updates.title !== void 0) {
|
|
4252
|
+
setClauses.push(`title = @p${paramIndex++}`);
|
|
4253
|
+
params.push(updates.title ?? null);
|
|
4254
|
+
}
|
|
4255
|
+
if (updates.metadata !== void 0) {
|
|
4256
|
+
setClauses.push(`metadata = @p${paramIndex++}`);
|
|
4257
|
+
params.push(JSON.stringify(updates.metadata));
|
|
4258
|
+
}
|
|
4259
|
+
params.push(chatId);
|
|
4260
|
+
const rows = await this.#query(
|
|
4261
|
+
`UPDATE chats SET ${setClauses.join(", ")} OUTPUT INSERTED.* WHERE id = @p${paramIndex}`,
|
|
4262
|
+
params
|
|
4263
|
+
);
|
|
4264
|
+
const row = rows[0];
|
|
4265
|
+
return {
|
|
4266
|
+
id: row.id,
|
|
4267
|
+
userId: row.userId,
|
|
4268
|
+
title: row.title ?? void 0,
|
|
4269
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
|
|
4270
|
+
createdAt: Number(row.createdAt),
|
|
4271
|
+
updatedAt: Number(row.updatedAt)
|
|
4272
|
+
};
|
|
4273
|
+
}
|
|
4274
|
+
async listChats(options) {
|
|
4275
|
+
const params = [];
|
|
4276
|
+
const whereClauses = [];
|
|
4277
|
+
let paramIndex = 0;
|
|
4278
|
+
if (options?.userId) {
|
|
4279
|
+
whereClauses.push(`c.userId = @p${paramIndex++}`);
|
|
4280
|
+
params.push(options.userId);
|
|
4281
|
+
}
|
|
4282
|
+
if (options?.metadata) {
|
|
4283
|
+
whereClauses.push(
|
|
4284
|
+
`JSON_VALUE(c.metadata, '$.' + @p${paramIndex}) = @p${paramIndex + 1}`
|
|
4285
|
+
);
|
|
4286
|
+
params.push(options.metadata.key);
|
|
4287
|
+
params.push(String(options.metadata.value));
|
|
4288
|
+
paramIndex += 2;
|
|
4289
|
+
}
|
|
4290
|
+
const whereClause = whereClauses.length > 0 ? `WHERE ${whereClauses.join(" AND ")}` : "";
|
|
4291
|
+
let paginationClause = "";
|
|
4292
|
+
if (options?.limit !== void 0) {
|
|
4293
|
+
paginationClause = ` OFFSET @p${paramIndex} ROWS FETCH NEXT @p${paramIndex + 1} ROWS ONLY`;
|
|
4294
|
+
params.push(options.offset ?? 0);
|
|
4295
|
+
params.push(options.limit);
|
|
4296
|
+
}
|
|
4297
|
+
const rows = await this.#query(
|
|
4298
|
+
`SELECT
|
|
4299
|
+
c.id,
|
|
4300
|
+
c.userId,
|
|
4301
|
+
c.title,
|
|
4302
|
+
c.metadata,
|
|
4303
|
+
c.createdAt,
|
|
4304
|
+
c.updatedAt,
|
|
4305
|
+
COUNT(DISTINCT m.id) as messageCount,
|
|
4306
|
+
COUNT(DISTINCT b.id) as branchCount
|
|
4307
|
+
FROM chats c
|
|
4308
|
+
LEFT JOIN messages m ON m.chatId = c.id
|
|
4309
|
+
LEFT JOIN branches b ON b.chatId = c.id
|
|
4310
|
+
${whereClause}
|
|
4311
|
+
GROUP BY c.id, c.userId, c.title, c.metadata, c.createdAt, c.updatedAt
|
|
4312
|
+
ORDER BY c.updatedAt DESC${paginationClause}`,
|
|
4313
|
+
params
|
|
4314
|
+
);
|
|
4315
|
+
return rows.map((row) => ({
|
|
4316
|
+
id: row.id,
|
|
4317
|
+
userId: row.userId,
|
|
4318
|
+
title: row.title ?? void 0,
|
|
4319
|
+
metadata: row.metadata ? JSON.parse(row.metadata) : void 0,
|
|
4320
|
+
messageCount: Number(row.messageCount),
|
|
4321
|
+
branchCount: Number(row.branchCount),
|
|
4322
|
+
createdAt: Number(row.createdAt),
|
|
4323
|
+
updatedAt: Number(row.updatedAt)
|
|
4324
|
+
}));
|
|
4325
|
+
}
|
|
4326
|
+
async deleteChat(chatId, options) {
|
|
4327
|
+
return this.#useTransaction(async (transaction) => {
|
|
4328
|
+
const mssql = _SqlServerContextStore.#requireMssql();
|
|
4329
|
+
const request = transaction.request();
|
|
4330
|
+
request.input("p0", mssql.NVarChar, chatId);
|
|
4331
|
+
let sql = "DELETE FROM chats WHERE id = @p0";
|
|
4332
|
+
if (options?.userId !== void 0) {
|
|
4333
|
+
request.input("p1", mssql.NVarChar, options.userId);
|
|
4334
|
+
sql += " AND userId = @p1";
|
|
4335
|
+
}
|
|
4336
|
+
const result = await request.query(sql);
|
|
4337
|
+
return (result.rowsAffected[0] ?? 0) > 0;
|
|
4338
|
+
});
|
|
4339
|
+
}
|
|
4340
|
+
// ==========================================================================
|
|
4341
|
+
// Message Operations (Graph Nodes)
|
|
4342
|
+
// ==========================================================================
|
|
4343
|
+
async addMessage(message2) {
|
|
4344
|
+
if (message2.parentId === message2.id) {
|
|
4345
|
+
throw new Error(`Message ${message2.id} cannot be its own parent`);
|
|
4346
|
+
}
|
|
4347
|
+
await this.#useTransaction(async (transaction) => {
|
|
4348
|
+
const mssql = _SqlServerContextStore.#requireMssql();
|
|
4349
|
+
const request = transaction.request();
|
|
4350
|
+
request.input("p0", mssql.NVarChar, message2.id);
|
|
4351
|
+
request.input("p1", mssql.NVarChar, message2.chatId);
|
|
4352
|
+
request.input("p2", mssql.NVarChar, message2.parentId);
|
|
4353
|
+
request.input("p3", mssql.NVarChar, message2.name);
|
|
4354
|
+
request.input("p4", mssql.NVarChar, message2.type ?? null);
|
|
4355
|
+
request.input("p5", mssql.NVarChar, JSON.stringify(message2.data));
|
|
4356
|
+
request.input("p6", mssql.BigInt, message2.createdAt);
|
|
4357
|
+
await request.query(`
|
|
4358
|
+
MERGE messages AS target
|
|
4359
|
+
USING (SELECT @p0 AS id) AS source
|
|
4360
|
+
ON target.id = source.id
|
|
4361
|
+
WHEN MATCHED THEN
|
|
4362
|
+
UPDATE SET name = @p3, type = @p4, data = @p5
|
|
4363
|
+
WHEN NOT MATCHED THEN
|
|
4364
|
+
INSERT (id, chatId, parentId, name, type, data, createdAt)
|
|
4365
|
+
VALUES (@p0, @p1, @p2, @p3, @p4, @p5, @p6);
|
|
4366
|
+
`);
|
|
4367
|
+
const content = typeof message2.data === "string" ? message2.data : JSON.stringify(message2.data);
|
|
4368
|
+
const ftsRequest = transaction.request();
|
|
4369
|
+
ftsRequest.input("p0", mssql.NVarChar, message2.id);
|
|
4370
|
+
ftsRequest.input("p1", mssql.NVarChar, message2.chatId);
|
|
4371
|
+
ftsRequest.input("p2", mssql.NVarChar, message2.name);
|
|
4372
|
+
ftsRequest.input("p3", mssql.NVarChar, content);
|
|
4373
|
+
await ftsRequest.query(`
|
|
4374
|
+
MERGE messages_fts AS target
|
|
4375
|
+
USING (SELECT @p0 AS messageId) AS source
|
|
4376
|
+
ON target.messageId = source.messageId
|
|
4377
|
+
WHEN MATCHED THEN
|
|
4378
|
+
UPDATE SET chatId = @p1, name = @p2, content = @p3
|
|
4379
|
+
WHEN NOT MATCHED THEN
|
|
4380
|
+
INSERT (messageId, chatId, name, content)
|
|
4381
|
+
VALUES (@p0, @p1, @p2, @p3);
|
|
4382
|
+
`);
|
|
4383
|
+
});
|
|
4384
|
+
}
|
|
4385
|
+
async getMessage(messageId) {
|
|
4386
|
+
const rows = await this.#query("SELECT * FROM messages WHERE id = @p0", [messageId]);
|
|
4387
|
+
if (rows.length === 0) {
|
|
4388
|
+
return void 0;
|
|
4389
|
+
}
|
|
4390
|
+
const row = rows[0];
|
|
4391
|
+
return {
|
|
4392
|
+
id: row.id,
|
|
4393
|
+
chatId: row.chatId,
|
|
4394
|
+
parentId: row.parentId,
|
|
4395
|
+
name: row.name,
|
|
4396
|
+
type: row.type ?? void 0,
|
|
4397
|
+
data: JSON.parse(row.data),
|
|
4398
|
+
createdAt: Number(row.createdAt)
|
|
4399
|
+
};
|
|
4400
|
+
}
|
|
4401
|
+
async getMessageChain(headId) {
|
|
4402
|
+
const rows = await this.#query(
|
|
4403
|
+
`WITH chain AS (
|
|
4404
|
+
SELECT *, 0 as depth FROM messages WHERE id = @p0
|
|
4405
|
+
UNION ALL
|
|
4406
|
+
SELECT m.*, c.depth + 1 FROM messages m
|
|
4407
|
+
INNER JOIN chain c ON m.id = c.parentId
|
|
4408
|
+
WHERE c.depth < 10000
|
|
4409
|
+
)
|
|
4410
|
+
SELECT * FROM chain
|
|
4411
|
+
ORDER BY depth DESC`,
|
|
4412
|
+
[headId]
|
|
4413
|
+
);
|
|
4414
|
+
return rows.map((row) => ({
|
|
4415
|
+
id: row.id,
|
|
4416
|
+
chatId: row.chatId,
|
|
4417
|
+
parentId: row.parentId,
|
|
4418
|
+
name: row.name,
|
|
4419
|
+
type: row.type ?? void 0,
|
|
4420
|
+
data: JSON.parse(row.data),
|
|
4421
|
+
createdAt: Number(row.createdAt)
|
|
4422
|
+
}));
|
|
4423
|
+
}
|
|
4424
|
+
async hasChildren(messageId) {
|
|
4425
|
+
const rows = await this.#query(
|
|
4426
|
+
`SELECT CASE WHEN EXISTS(SELECT 1 FROM messages WHERE parentId = @p0) THEN 1 ELSE 0 END as hasChildren`,
|
|
4427
|
+
[messageId]
|
|
4428
|
+
);
|
|
4429
|
+
return rows[0].hasChildren === 1;
|
|
4430
|
+
}
|
|
4431
|
+
async getMessages(chatId) {
|
|
4432
|
+
const chat = await this.getChat(chatId);
|
|
4433
|
+
if (!chat) {
|
|
4434
|
+
throw new Error(`Chat "${chatId}" not found`);
|
|
4435
|
+
}
|
|
4436
|
+
const activeBranch = await this.getActiveBranch(chatId);
|
|
4437
|
+
if (!activeBranch?.headMessageId) {
|
|
4438
|
+
return [];
|
|
4439
|
+
}
|
|
4440
|
+
return this.getMessageChain(activeBranch.headMessageId);
|
|
4441
|
+
}
|
|
4442
|
+
// ==========================================================================
|
|
4443
|
+
// Branch Operations
|
|
4444
|
+
// ==========================================================================
|
|
4445
|
+
async createBranch(branch) {
|
|
4446
|
+
await this.#query(
|
|
4447
|
+
`INSERT INTO branches (id, chatId, name, headMessageId, isActive, createdAt)
|
|
4448
|
+
VALUES (@p0, @p1, @p2, @p3, @p4, @p5)`,
|
|
4449
|
+
[
|
|
4450
|
+
branch.id,
|
|
4451
|
+
branch.chatId,
|
|
4452
|
+
branch.name,
|
|
4453
|
+
branch.headMessageId,
|
|
4454
|
+
branch.isActive ? 1 : 0,
|
|
4455
|
+
branch.createdAt
|
|
4456
|
+
]
|
|
4457
|
+
);
|
|
4458
|
+
}
|
|
4459
|
+
async getBranch(chatId, name) {
|
|
4460
|
+
const rows = await this.#query("SELECT * FROM branches WHERE chatId = @p0 AND name = @p1", [
|
|
4461
|
+
chatId,
|
|
4462
|
+
name
|
|
4463
|
+
]);
|
|
4464
|
+
if (rows.length === 0) {
|
|
4465
|
+
return void 0;
|
|
4466
|
+
}
|
|
4467
|
+
const row = rows[0];
|
|
4468
|
+
return {
|
|
4469
|
+
id: row.id,
|
|
4470
|
+
chatId: row.chatId,
|
|
4471
|
+
name: row.name,
|
|
4472
|
+
headMessageId: row.headMessageId,
|
|
4473
|
+
isActive: row.isActive === true || row.isActive === 1,
|
|
4474
|
+
createdAt: Number(row.createdAt)
|
|
4475
|
+
};
|
|
4476
|
+
}
|
|
4477
|
+
async getActiveBranch(chatId) {
|
|
4478
|
+
const rows = await this.#query("SELECT * FROM branches WHERE chatId = @p0 AND isActive = 1", [chatId]);
|
|
4479
|
+
if (rows.length === 0) {
|
|
4480
|
+
return void 0;
|
|
4481
|
+
}
|
|
4482
|
+
const row = rows[0];
|
|
4483
|
+
return {
|
|
4484
|
+
id: row.id,
|
|
4485
|
+
chatId: row.chatId,
|
|
4486
|
+
name: row.name,
|
|
4487
|
+
headMessageId: row.headMessageId,
|
|
4488
|
+
isActive: true,
|
|
4489
|
+
createdAt: Number(row.createdAt)
|
|
4490
|
+
};
|
|
4491
|
+
}
|
|
4492
|
+
async setActiveBranch(chatId, branchId) {
|
|
4493
|
+
await this.#useTransaction(async (transaction) => {
|
|
4494
|
+
const mssql = _SqlServerContextStore.#requireMssql();
|
|
4495
|
+
const deactivateRequest = transaction.request();
|
|
4496
|
+
deactivateRequest.input("p0", mssql.NVarChar, chatId);
|
|
4497
|
+
await deactivateRequest.query(
|
|
4498
|
+
"UPDATE branches SET isActive = 0 WHERE chatId = @p0"
|
|
4499
|
+
);
|
|
4500
|
+
const activateRequest = transaction.request();
|
|
4501
|
+
activateRequest.input("p0", mssql.NVarChar, branchId);
|
|
4502
|
+
await activateRequest.query(
|
|
4503
|
+
"UPDATE branches SET isActive = 1 WHERE id = @p0"
|
|
4504
|
+
);
|
|
4505
|
+
});
|
|
4506
|
+
}
|
|
4507
|
+
async updateBranchHead(branchId, messageId) {
|
|
4508
|
+
await this.#query(
|
|
4509
|
+
"UPDATE branches SET headMessageId = @p0 WHERE id = @p1",
|
|
4510
|
+
[messageId, branchId]
|
|
4511
|
+
);
|
|
4512
|
+
}
|
|
4513
|
+
async listBranches(chatId) {
|
|
4514
|
+
const branches = await this.#query(
|
|
4515
|
+
`SELECT
|
|
4516
|
+
id,
|
|
4517
|
+
name,
|
|
4518
|
+
headMessageId,
|
|
4519
|
+
isActive,
|
|
4520
|
+
createdAt
|
|
4521
|
+
FROM branches
|
|
4522
|
+
WHERE chatId = @p0
|
|
4523
|
+
ORDER BY createdAt ASC`,
|
|
4524
|
+
[chatId]
|
|
4525
|
+
);
|
|
4526
|
+
const result = [];
|
|
4527
|
+
for (const branch of branches) {
|
|
4528
|
+
let messageCount = 0;
|
|
4529
|
+
if (branch.headMessageId) {
|
|
4530
|
+
const countRows = await this.#query(
|
|
4531
|
+
`WITH chain AS (
|
|
4532
|
+
SELECT id, parentId FROM messages WHERE id = @p0
|
|
4533
|
+
UNION ALL
|
|
4534
|
+
SELECT m.id, m.parentId FROM messages m
|
|
4535
|
+
INNER JOIN chain c ON m.id = c.parentId
|
|
4536
|
+
)
|
|
4537
|
+
SELECT COUNT(*) as count FROM chain`,
|
|
4538
|
+
[branch.headMessageId]
|
|
4539
|
+
);
|
|
4540
|
+
messageCount = Number(countRows[0].count);
|
|
4541
|
+
}
|
|
4542
|
+
result.push({
|
|
4543
|
+
id: branch.id,
|
|
4544
|
+
name: branch.name,
|
|
4545
|
+
headMessageId: branch.headMessageId,
|
|
4546
|
+
isActive: branch.isActive === true || branch.isActive === 1,
|
|
4547
|
+
messageCount,
|
|
4548
|
+
createdAt: Number(branch.createdAt)
|
|
4549
|
+
});
|
|
4550
|
+
}
|
|
4551
|
+
return result;
|
|
4552
|
+
}
|
|
4553
|
+
// ==========================================================================
|
|
4554
|
+
// Checkpoint Operations
|
|
4555
|
+
// ==========================================================================
|
|
4556
|
+
async createCheckpoint(checkpoint) {
|
|
4557
|
+
await this.#useTransaction(async (transaction) => {
|
|
4558
|
+
const mssql = _SqlServerContextStore.#requireMssql();
|
|
4559
|
+
const request = transaction.request();
|
|
4560
|
+
request.input("p0", mssql.NVarChar, checkpoint.id);
|
|
4561
|
+
request.input("p1", mssql.NVarChar, checkpoint.chatId);
|
|
4562
|
+
request.input("p2", mssql.NVarChar, checkpoint.name);
|
|
4563
|
+
request.input("p3", mssql.NVarChar, checkpoint.messageId);
|
|
4564
|
+
request.input("p4", mssql.BigInt, checkpoint.createdAt);
|
|
4565
|
+
await request.query(`
|
|
4566
|
+
MERGE checkpoints AS target
|
|
4567
|
+
USING (SELECT @p1 AS chatId, @p2 AS name) AS source
|
|
4568
|
+
ON target.chatId = source.chatId AND target.name = source.name
|
|
4569
|
+
WHEN MATCHED THEN
|
|
4570
|
+
UPDATE SET messageId = @p3, createdAt = @p4
|
|
4571
|
+
WHEN NOT MATCHED THEN
|
|
4572
|
+
INSERT (id, chatId, name, messageId, createdAt)
|
|
4573
|
+
VALUES (@p0, @p1, @p2, @p3, @p4);
|
|
4574
|
+
`);
|
|
4575
|
+
});
|
|
4576
|
+
}
|
|
4577
|
+
async getCheckpoint(chatId, name) {
|
|
4578
|
+
const rows = await this.#query("SELECT * FROM checkpoints WHERE chatId = @p0 AND name = @p1", [
|
|
4579
|
+
chatId,
|
|
4580
|
+
name
|
|
4581
|
+
]);
|
|
4582
|
+
if (rows.length === 0) {
|
|
4583
|
+
return void 0;
|
|
4584
|
+
}
|
|
4585
|
+
const row = rows[0];
|
|
4586
|
+
return {
|
|
4587
|
+
id: row.id,
|
|
4588
|
+
chatId: row.chatId,
|
|
4589
|
+
name: row.name,
|
|
4590
|
+
messageId: row.messageId,
|
|
4591
|
+
createdAt: Number(row.createdAt)
|
|
4592
|
+
};
|
|
4593
|
+
}
|
|
4594
|
+
async listCheckpoints(chatId) {
|
|
4595
|
+
const rows = await this.#query(
|
|
4596
|
+
`SELECT id, name, messageId, createdAt
|
|
4597
|
+
FROM checkpoints
|
|
4598
|
+
WHERE chatId = @p0
|
|
4599
|
+
ORDER BY createdAt DESC`,
|
|
4600
|
+
[chatId]
|
|
4601
|
+
);
|
|
4602
|
+
return rows.map((row) => ({
|
|
4603
|
+
id: row.id,
|
|
4604
|
+
name: row.name,
|
|
4605
|
+
messageId: row.messageId,
|
|
4606
|
+
createdAt: Number(row.createdAt)
|
|
4607
|
+
}));
|
|
4608
|
+
}
|
|
4609
|
+
async deleteCheckpoint(chatId, name) {
|
|
4610
|
+
await this.#query(
|
|
4611
|
+
"DELETE FROM checkpoints WHERE chatId = @p0 AND name = @p1",
|
|
4612
|
+
[chatId, name]
|
|
4613
|
+
);
|
|
4614
|
+
}
|
|
4615
|
+
// ==========================================================================
|
|
4616
|
+
// Search Operations
|
|
4617
|
+
// ==========================================================================
|
|
4618
|
+
async searchMessages(chatId, query, options) {
|
|
4619
|
+
const limit = options?.limit ?? 20;
|
|
4620
|
+
const roles = options?.roles;
|
|
4621
|
+
const ftsCheck = await this.#query(
|
|
4622
|
+
`SELECT CAST(SERVERPROPERTY('IsFullTextInstalled') AS INT) as ftsInstalled`
|
|
4623
|
+
);
|
|
4624
|
+
const ftsAvailable = ftsCheck[0]?.ftsInstalled === 1;
|
|
4625
|
+
if (ftsAvailable) {
|
|
4626
|
+
let sql = `
|
|
4627
|
+
SELECT
|
|
4628
|
+
m.id,
|
|
4629
|
+
m.chatId,
|
|
4630
|
+
m.parentId,
|
|
4631
|
+
m.name,
|
|
4632
|
+
m.type,
|
|
4633
|
+
m.data,
|
|
4634
|
+
m.createdAt,
|
|
4635
|
+
ct.RANK as rank,
|
|
4636
|
+
SUBSTRING(fts.content, 1, 200) as snippet
|
|
4637
|
+
FROM messages_fts fts
|
|
4638
|
+
INNER JOIN CONTAINSTABLE(messages_fts, content, @p0) ct
|
|
4639
|
+
ON fts.messageId = ct.[KEY]
|
|
4640
|
+
INNER JOIN messages m ON m.id = fts.messageId
|
|
4641
|
+
WHERE fts.chatId = @p1
|
|
4642
|
+
`;
|
|
4643
|
+
const params = [query, chatId];
|
|
4644
|
+
let paramIndex = 2;
|
|
4645
|
+
if (roles && roles.length > 0) {
|
|
4646
|
+
const placeholders = roles.map(() => `@p${paramIndex++}`).join(", ");
|
|
4647
|
+
sql += ` AND fts.name IN (${placeholders})`;
|
|
4648
|
+
params.push(...roles);
|
|
4649
|
+
}
|
|
4650
|
+
sql += ` ORDER BY ct.RANK DESC OFFSET 0 ROWS FETCH NEXT @p${paramIndex} ROWS ONLY`;
|
|
4651
|
+
params.push(limit);
|
|
4652
|
+
const rows = await this.#query(sql, params);
|
|
4653
|
+
return rows.map((row) => ({
|
|
4654
|
+
message: {
|
|
4655
|
+
id: row.id,
|
|
4656
|
+
chatId: row.chatId,
|
|
4657
|
+
parentId: row.parentId,
|
|
4658
|
+
name: row.name,
|
|
4659
|
+
type: row.type ?? void 0,
|
|
4660
|
+
data: JSON.parse(row.data),
|
|
4661
|
+
createdAt: Number(row.createdAt)
|
|
4662
|
+
},
|
|
4663
|
+
rank: row.rank,
|
|
4664
|
+
snippet: row.snippet
|
|
4665
|
+
}));
|
|
4666
|
+
} else {
|
|
4667
|
+
let sql = `
|
|
4668
|
+
SELECT
|
|
4669
|
+
m.id,
|
|
4670
|
+
m.chatId,
|
|
4671
|
+
m.parentId,
|
|
4672
|
+
m.name,
|
|
4673
|
+
m.type,
|
|
4674
|
+
m.data,
|
|
4675
|
+
m.createdAt,
|
|
4676
|
+
1 as rank,
|
|
4677
|
+
SUBSTRING(fts.content, 1, 200) as snippet
|
|
4678
|
+
FROM messages_fts fts
|
|
4679
|
+
INNER JOIN messages m ON m.id = fts.messageId
|
|
4680
|
+
WHERE fts.chatId = @p0 AND fts.content LIKE '%' + @p1 + '%'
|
|
4681
|
+
`;
|
|
4682
|
+
const params = [chatId, query];
|
|
4683
|
+
let paramIndex = 2;
|
|
4684
|
+
if (roles && roles.length > 0) {
|
|
4685
|
+
const placeholders = roles.map(() => `@p${paramIndex++}`).join(", ");
|
|
4686
|
+
sql += ` AND fts.name IN (${placeholders})`;
|
|
4687
|
+
params.push(...roles);
|
|
4688
|
+
}
|
|
4689
|
+
sql += ` ORDER BY m.createdAt DESC OFFSET 0 ROWS FETCH NEXT @p${paramIndex} ROWS ONLY`;
|
|
4690
|
+
params.push(limit);
|
|
4691
|
+
const rows = await this.#query(sql, params);
|
|
4692
|
+
return rows.map((row) => ({
|
|
4693
|
+
message: {
|
|
4694
|
+
id: row.id,
|
|
4695
|
+
chatId: row.chatId,
|
|
4696
|
+
parentId: row.parentId,
|
|
4697
|
+
name: row.name,
|
|
4698
|
+
type: row.type ?? void 0,
|
|
4699
|
+
data: JSON.parse(row.data),
|
|
4700
|
+
createdAt: Number(row.createdAt)
|
|
4701
|
+
},
|
|
4702
|
+
rank: row.rank,
|
|
4703
|
+
snippet: row.snippet
|
|
4704
|
+
}));
|
|
4705
|
+
}
|
|
4706
|
+
}
|
|
4707
|
+
// ==========================================================================
|
|
4708
|
+
// Visualization Operations
|
|
4709
|
+
// ==========================================================================
|
|
4710
|
+
async getGraph(chatId) {
|
|
4711
|
+
const messageRows = await this.#query(
|
|
4712
|
+
`SELECT id, parentId, name, data, createdAt
|
|
4713
|
+
FROM messages
|
|
4714
|
+
WHERE chatId = @p0
|
|
4715
|
+
ORDER BY createdAt ASC`,
|
|
4716
|
+
[chatId]
|
|
4717
|
+
);
|
|
4718
|
+
const nodes = messageRows.map((row) => {
|
|
4719
|
+
const data = JSON.parse(row.data);
|
|
4720
|
+
const content = typeof data === "string" ? data : JSON.stringify(data);
|
|
4721
|
+
return {
|
|
4722
|
+
id: row.id,
|
|
4723
|
+
parentId: row.parentId,
|
|
4724
|
+
role: row.name,
|
|
4725
|
+
content: content.length > 50 ? content.slice(0, 50) + "..." : content,
|
|
4726
|
+
createdAt: Number(row.createdAt)
|
|
4727
|
+
};
|
|
4728
|
+
});
|
|
4729
|
+
const branchRows = await this.#query(
|
|
4730
|
+
`SELECT name, headMessageId, isActive
|
|
4731
|
+
FROM branches
|
|
4732
|
+
WHERE chatId = @p0
|
|
4733
|
+
ORDER BY createdAt ASC`,
|
|
4734
|
+
[chatId]
|
|
4735
|
+
);
|
|
4736
|
+
const branches = branchRows.map((row) => ({
|
|
4737
|
+
name: row.name,
|
|
4738
|
+
headMessageId: row.headMessageId,
|
|
4739
|
+
isActive: row.isActive === true || row.isActive === 1
|
|
4740
|
+
}));
|
|
4741
|
+
const checkpointRows = await this.#query(
|
|
4742
|
+
`SELECT name, messageId
|
|
4743
|
+
FROM checkpoints
|
|
4744
|
+
WHERE chatId = @p0
|
|
4745
|
+
ORDER BY createdAt ASC`,
|
|
4746
|
+
[chatId]
|
|
4747
|
+
);
|
|
4748
|
+
const checkpoints = checkpointRows.map((row) => ({
|
|
4749
|
+
name: row.name,
|
|
4750
|
+
messageId: row.messageId
|
|
4751
|
+
}));
|
|
4752
|
+
return {
|
|
4753
|
+
chatId,
|
|
4754
|
+
nodes,
|
|
4755
|
+
branches,
|
|
4756
|
+
checkpoints
|
|
4757
|
+
};
|
|
4758
|
+
}
|
|
4759
|
+
};
|
|
4760
|
+
|
|
4761
|
+
// packages/context/src/lib/visualize.ts
|
|
4762
|
+
function visualizeGraph(data) {
|
|
4763
|
+
if (data.nodes.length === 0) {
|
|
4764
|
+
return `[chat: ${data.chatId}]
|
|
4765
|
+
|
|
4766
|
+
(empty)`;
|
|
4767
|
+
}
|
|
4768
|
+
const childrenByParentId = /* @__PURE__ */ new Map();
|
|
4769
|
+
const branchHeads = /* @__PURE__ */ new Map();
|
|
4770
|
+
const checkpointsByMessageId = /* @__PURE__ */ new Map();
|
|
4771
|
+
for (const node of data.nodes) {
|
|
4772
|
+
const children = childrenByParentId.get(node.parentId) ?? [];
|
|
4773
|
+
children.push(node);
|
|
4774
|
+
childrenByParentId.set(node.parentId, children);
|
|
4775
|
+
}
|
|
4776
|
+
for (const branch of data.branches) {
|
|
4777
|
+
if (branch.headMessageId) {
|
|
4778
|
+
const heads = branchHeads.get(branch.headMessageId) ?? [];
|
|
4779
|
+
heads.push(branch.isActive ? `${branch.name} *` : branch.name);
|
|
4780
|
+
branchHeads.set(branch.headMessageId, heads);
|
|
4781
|
+
}
|
|
4782
|
+
}
|
|
4783
|
+
for (const checkpoint of data.checkpoints) {
|
|
4784
|
+
const cps = checkpointsByMessageId.get(checkpoint.messageId) ?? [];
|
|
4785
|
+
cps.push(checkpoint.name);
|
|
4786
|
+
checkpointsByMessageId.set(checkpoint.messageId, cps);
|
|
4787
|
+
}
|
|
4788
|
+
const roots = childrenByParentId.get(null) ?? [];
|
|
4789
|
+
const lines = [`[chat: ${data.chatId}]`, ""];
|
|
4790
|
+
function renderNode(node, prefix, isLast, isRoot) {
|
|
4791
|
+
const connector = isRoot ? "" : isLast ? "\u2514\u2500\u2500 " : "\u251C\u2500\u2500 ";
|
|
4792
|
+
const contentPreview = node.content.replace(/\n/g, " ");
|
|
4793
|
+
let line = `${prefix}${connector}${node.id.slice(0, 8)} (${node.role}): "${contentPreview}"`;
|
|
4794
|
+
const branches = branchHeads.get(node.id);
|
|
4795
|
+
if (branches) {
|
|
4796
|
+
line += ` <- [${branches.join(", ")}]`;
|
|
4797
|
+
}
|
|
4798
|
+
const checkpoints = checkpointsByMessageId.get(node.id);
|
|
4799
|
+
if (checkpoints) {
|
|
4800
|
+
line += ` {${checkpoints.join(", ")}}`;
|
|
4801
|
+
}
|
|
4802
|
+
lines.push(line);
|
|
4803
|
+
const children = childrenByParentId.get(node.id) ?? [];
|
|
4804
|
+
const childPrefix = isRoot ? "" : prefix + (isLast ? " " : "\u2502 ");
|
|
4805
|
+
for (let i = 0; i < children.length; i++) {
|
|
4806
|
+
renderNode(children[i], childPrefix, i === children.length - 1, false);
|
|
4807
|
+
}
|
|
4808
|
+
}
|
|
4809
|
+
for (let i = 0; i < roots.length; i++) {
|
|
4810
|
+
renderNode(roots[i], "", i === roots.length - 1, true);
|
|
4811
|
+
}
|
|
4812
|
+
lines.push("");
|
|
4813
|
+
lines.push("Legend: * = active branch, {...} = checkpoint");
|
|
4814
|
+
return lines.join("\n");
|
|
4815
|
+
}
|
|
4816
|
+
|
|
4817
|
+
// packages/context/src/lib/agent.ts
|
|
4818
|
+
import { groq } from "@ai-sdk/groq";
|
|
4819
|
+
import {
|
|
4820
|
+
NoSuchToolError,
|
|
4821
|
+
Output,
|
|
4822
|
+
convertToModelMessages,
|
|
4823
|
+
createUIMessageStream,
|
|
4824
|
+
generateId as generateId2,
|
|
4825
|
+
generateText,
|
|
4826
|
+
smoothStream,
|
|
4827
|
+
stepCountIs,
|
|
4828
|
+
streamText
|
|
4829
|
+
} from "ai";
|
|
4830
|
+
import chalk2 from "chalk";
|
|
4831
|
+
import "zod";
|
|
4832
|
+
import "@deepagents/agent";
|
|
4833
|
+
var Agent = class _Agent {
|
|
4834
|
+
#options;
|
|
4835
|
+
#guardrails = [];
|
|
4836
|
+
tools;
|
|
4837
|
+
constructor(options) {
|
|
4838
|
+
this.#options = options;
|
|
4839
|
+
this.tools = options.tools || {};
|
|
4840
|
+
this.#guardrails = options.guardrails || [];
|
|
4841
|
+
}
|
|
4842
|
+
async generate(contextVariables, config) {
|
|
4843
|
+
if (!this.#options.context) {
|
|
4844
|
+
throw new Error(`Agent ${this.#options.name} is missing a context.`);
|
|
4845
|
+
}
|
|
4846
|
+
if (!this.#options.model) {
|
|
4847
|
+
throw new Error(`Agent ${this.#options.name} is missing a model.`);
|
|
4848
|
+
}
|
|
4849
|
+
const { messages, systemPrompt } = await this.#options.context.resolve({
|
|
4850
|
+
renderer: new XmlRenderer()
|
|
4851
|
+
});
|
|
4852
|
+
return generateText({
|
|
4853
|
+
abortSignal: config?.abortSignal,
|
|
4854
|
+
providerOptions: this.#options.providerOptions,
|
|
4855
|
+
model: this.#options.model,
|
|
4856
|
+
system: systemPrompt,
|
|
4857
|
+
messages: await convertToModelMessages(messages),
|
|
4858
|
+
stopWhen: stepCountIs(25),
|
|
4859
|
+
tools: this.#options.tools,
|
|
4860
|
+
experimental_context: contextVariables,
|
|
4861
|
+
experimental_repairToolCall: repairToolCall,
|
|
4862
|
+
toolChoice: this.#options.toolChoice,
|
|
4863
|
+
onStepFinish: (step) => {
|
|
4864
|
+
const toolCall = step.toolCalls.at(-1);
|
|
4865
|
+
if (toolCall) {
|
|
4866
|
+
console.log(
|
|
4867
|
+
`Debug: ${chalk2.yellow("ToolCalled")}: ${toolCall.toolName}(${JSON.stringify(toolCall.input)})`
|
|
4868
|
+
);
|
|
4869
|
+
}
|
|
4870
|
+
}
|
|
4871
|
+
});
|
|
4872
|
+
}
|
|
4873
|
+
/**
|
|
4874
|
+
* Stream a response from the agent.
|
|
4875
|
+
*
|
|
4876
|
+
* When guardrails are configured, `toUIMessageStream()` is wrapped to provide
|
|
4877
|
+
* self-correction behavior. Direct access to fullStream/textStream bypasses guardrails.
|
|
4878
|
+
*
|
|
4879
|
+
* @example
|
|
4880
|
+
* ```typescript
|
|
4881
|
+
* const stream = await agent.stream({});
|
|
4882
|
+
*
|
|
4883
|
+
* // With guardrails - use toUIMessageStream for protection
|
|
4884
|
+
* await printer.readableStream(stream.toUIMessageStream());
|
|
4885
|
+
*
|
|
4886
|
+
* // Or use printer.stdout which uses toUIMessageStream internally
|
|
4887
|
+
* await printer.stdout(stream);
|
|
4888
|
+
* ```
|
|
4889
|
+
*/
|
|
4890
|
+
async stream(contextVariables, config) {
|
|
4891
|
+
if (!this.#options.context) {
|
|
4892
|
+
throw new Error(`Agent ${this.#options.name} is missing a context.`);
|
|
4893
|
+
}
|
|
4894
|
+
if (!this.#options.model) {
|
|
4895
|
+
throw new Error(`Agent ${this.#options.name} is missing a model.`);
|
|
4896
|
+
}
|
|
4897
|
+
const result = await this.#createRawStream(contextVariables, config);
|
|
4898
|
+
if (this.#guardrails.length === 0) {
|
|
4899
|
+
return result;
|
|
4900
|
+
}
|
|
4901
|
+
return this.#wrapWithGuardrails(result, contextVariables, config);
|
|
4902
|
+
}
|
|
4903
|
+
/**
|
|
4904
|
+
* Create a raw stream without guardrail processing.
|
|
4905
|
+
*/
|
|
4906
|
+
async #createRawStream(contextVariables, config) {
|
|
3566
4907
|
const { messages, systemPrompt } = await this.#options.context.resolve({
|
|
3567
4908
|
renderer: new XmlRenderer()
|
|
3568
4909
|
});
|
|
@@ -3607,8 +4948,10 @@ var Agent = class _Agent {
|
|
|
3607
4948
|
execute: async ({ writer }) => {
|
|
3608
4949
|
let currentResult = result;
|
|
3609
4950
|
let attempt = 0;
|
|
4951
|
+
const { mounts } = context.getSkillMounts();
|
|
3610
4952
|
const guardrailContext = {
|
|
3611
|
-
availableTools: Object.keys(this.tools)
|
|
4953
|
+
availableTools: Object.keys(this.tools),
|
|
4954
|
+
availableSkills: mounts
|
|
3612
4955
|
};
|
|
3613
4956
|
while (attempt < maxRetries) {
|
|
3614
4957
|
if (config?.abortSignal?.aborted) {
|
|
@@ -3636,10 +4979,20 @@ var Agent = class _Agent {
|
|
|
3636
4979
|
);
|
|
3637
4980
|
break;
|
|
3638
4981
|
}
|
|
4982
|
+
if (checkResult.type === "stop") {
|
|
4983
|
+
console.log(
|
|
4984
|
+
chalk2.red(
|
|
4985
|
+
`[${this.#options.name}] Guardrail stopped - unrecoverable error, no retry`
|
|
4986
|
+
)
|
|
4987
|
+
);
|
|
4988
|
+
writer.write(part);
|
|
4989
|
+
writer.write({ type: "finish" });
|
|
4990
|
+
return;
|
|
4991
|
+
}
|
|
3639
4992
|
if (checkResult.part.type === "text-delta") {
|
|
3640
4993
|
accumulatedText += checkResult.part.delta;
|
|
3641
4994
|
}
|
|
3642
|
-
writer.write(
|
|
4995
|
+
writer.write(part);
|
|
3643
4996
|
}
|
|
3644
4997
|
if (!guardrailFailed) {
|
|
3645
4998
|
writer.write({ type: "finish" });
|
|
@@ -3806,7 +5159,9 @@ export {
|
|
|
3806
5159
|
ModelsRegistry,
|
|
3807
5160
|
MountPathError,
|
|
3808
5161
|
PackageInstallError,
|
|
5162
|
+
PostgresContextStore,
|
|
3809
5163
|
RuntimeStrategy,
|
|
5164
|
+
SqlServerContextStore,
|
|
3810
5165
|
SqliteContextStore,
|
|
3811
5166
|
TomlRenderer,
|
|
3812
5167
|
ToonRenderer,
|
|
@@ -3854,6 +5209,7 @@ export {
|
|
|
3854
5209
|
role,
|
|
3855
5210
|
runGuardrailChain,
|
|
3856
5211
|
skills,
|
|
5212
|
+
stop,
|
|
3857
5213
|
structuredOutput,
|
|
3858
5214
|
styleGuide,
|
|
3859
5215
|
term,
|