dsh-trivium 0.2.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/LICENSE +21 -0
- package/README.md +112 -0
- package/cordis.patch.yml +11 -0
- package/lib/client.js +804 -0
- package/lib/embed.js +110 -0
- package/lib/extract.js +454 -0
- package/lib/index.js +388 -0
- package/lib/schema.js +66 -0
- package/lib/server.js +222 -0
- package/lib/settings.js +68 -0
- package/lib/store.js +1042 -0
- package/lib/tokens.js +28 -0
- package/lib/tools.js +216 -0
- package/lib/until.js +85 -0
- package/package.json +68 -0
package/lib/tokens.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Cheap, checkable token estimate for injection budgets (CJK ≈ 1, ASCII ≈ 1/4). */
|
|
2
|
+
|
|
3
|
+
export function estimateTokens(text) {
|
|
4
|
+
const s = String(text || "");
|
|
5
|
+
let tokens = 0;
|
|
6
|
+
for (const ch of s) {
|
|
7
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
8
|
+
if (code <= 0x7f) tokens += 0.25;
|
|
9
|
+
else if (code <= 0x7ff) tokens += 0.5;
|
|
10
|
+
else tokens += 1;
|
|
11
|
+
}
|
|
12
|
+
return Math.ceil(tokens);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function clipToTokenBudget(text, budget) {
|
|
16
|
+
const raw = String(text || "");
|
|
17
|
+
const cap = Math.max(1, Number(budget) || 400);
|
|
18
|
+
if (estimateTokens(raw) <= cap) return raw;
|
|
19
|
+
let lo = 0;
|
|
20
|
+
let hi = raw.length;
|
|
21
|
+
while (lo < hi) {
|
|
22
|
+
const mid = Math.ceil((lo + hi) / 2);
|
|
23
|
+
if (estimateTokens(raw.slice(0, mid)) <= cap) lo = mid;
|
|
24
|
+
else hi = mid - 1;
|
|
25
|
+
}
|
|
26
|
+
const cut = Math.max(0, lo - 1);
|
|
27
|
+
return `${raw.slice(0, cut)}…`;
|
|
28
|
+
}
|
package/lib/tools.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
2
|
+
import { EDGE_LABELS, looksSecret } from "./schema.js";
|
|
3
|
+
import {
|
|
4
|
+
dbPathFor,
|
|
5
|
+
embedQueryVector,
|
|
6
|
+
findMergeTarget,
|
|
7
|
+
formatHit,
|
|
8
|
+
insertNode,
|
|
9
|
+
listIncomingBusiness,
|
|
10
|
+
mergeInto,
|
|
11
|
+
openWorkspaceDb,
|
|
12
|
+
searchNodes,
|
|
13
|
+
ensureLink,
|
|
14
|
+
} from "./store.js";
|
|
15
|
+
|
|
16
|
+
const WRITE_TOOLS = new Set(["ctx_remember", "ctx_link"]);
|
|
17
|
+
|
|
18
|
+
const TEXT_OUTPUT = {
|
|
19
|
+
schema: { type: "string" },
|
|
20
|
+
render: (_args, value) => [{ type: "text", text: value }],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function textTool(definition) {
|
|
24
|
+
const write = WRITE_TOOLS.has(definition.name);
|
|
25
|
+
return defineTool({
|
|
26
|
+
...definition,
|
|
27
|
+
output: TEXT_OUTPUT,
|
|
28
|
+
isConcurrencySafe: write ? () => false : () => true,
|
|
29
|
+
presentCall: (args) => ({
|
|
30
|
+
card: "generic",
|
|
31
|
+
kind: write ? "edit" : definition.name === "ctx_find" ? "search" : "read",
|
|
32
|
+
title: definition.name,
|
|
33
|
+
rawInput: args,
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function cwdOf(agentOrExec) {
|
|
39
|
+
const agent = agentOrExec?.agent ?? agentOrExec;
|
|
40
|
+
return (
|
|
41
|
+
agent?.session?.header?.cwd ||
|
|
42
|
+
process.env.DSH_CWD ||
|
|
43
|
+
process.cwd()
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function dbOf(exec) {
|
|
48
|
+
return openWorkspaceDb(cwdOf(exec));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function registerTools(ctx, optionsOrFn) {
|
|
52
|
+
const optionsOf = typeof optionsOrFn === "function" ? optionsOrFn : () => optionsOrFn || {};
|
|
53
|
+
const writeApprovalOf = () => optionsOf().writeApproval === true;
|
|
54
|
+
|
|
55
|
+
ctx.effect(
|
|
56
|
+
() =>
|
|
57
|
+
ctx.tools.register(
|
|
58
|
+
textTool({
|
|
59
|
+
name: "ctx_find",
|
|
60
|
+
description:
|
|
61
|
+
"Search local Trivium graph memory. If the query names a known entity, also returns its unexpired about/decided/broke/fixed neighbors. Returns L0 summaries plus edge paths (incoming as <-label-id). Call ctx_read for full text. Do not paste find results verbatim into later thinking.",
|
|
62
|
+
parameters: {
|
|
63
|
+
query: { type: "string", required: true, description: "Search query." },
|
|
64
|
+
top_k: { type: "integer", description: "Max hits, 1-20." },
|
|
65
|
+
},
|
|
66
|
+
async execute(args, exec) {
|
|
67
|
+
try {
|
|
68
|
+
const options = optionsOf();
|
|
69
|
+
const db = await dbOf(exec);
|
|
70
|
+
const k = Math.max(1, Math.min(20, args.top_k || options.topK || 8));
|
|
71
|
+
const queryVector = await embedQueryVector(args.query);
|
|
72
|
+
const hits = searchNodes(db, args.query, {
|
|
73
|
+
topK: k,
|
|
74
|
+
expandDepth: options.expandDepth ?? 1,
|
|
75
|
+
queryVector: queryVector || undefined,
|
|
76
|
+
});
|
|
77
|
+
if (!hits.length) return "No memory hits.";
|
|
78
|
+
const grouped = [];
|
|
79
|
+
let lastType = "";
|
|
80
|
+
for (const hit of hits) {
|
|
81
|
+
const row = formatHit(db, hit);
|
|
82
|
+
if (row.type !== lastType) {
|
|
83
|
+
grouped.push(`## ${row.type}`);
|
|
84
|
+
lastType = row.type;
|
|
85
|
+
}
|
|
86
|
+
grouped.push(
|
|
87
|
+
`[${row.score.toFixed(3)}] id=${row.id} type=${row.type}${row.until ? ` until=${row.until}` : ""}${row.stale ? " stale" : ""}\n${row.l0}\npath: ${row.path.join(" | ") || "(none)"}`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
return grouped.join("\n\n");
|
|
91
|
+
} catch (err) {
|
|
92
|
+
return `ctx_find unavailable: ${err.message}`;
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
}),
|
|
96
|
+
),
|
|
97
|
+
"dsh-trivium.ctx_find",
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
ctx.effect(
|
|
101
|
+
() =>
|
|
102
|
+
ctx.tools.register(
|
|
103
|
+
textTool({
|
|
104
|
+
name: "ctx_read",
|
|
105
|
+
description: "Read one memory node by numeric id returned from ctx_find. Includes incoming about/decided/broke/fixed edges.",
|
|
106
|
+
parameters: {
|
|
107
|
+
id: { type: "integer", required: true, description: "Node id." },
|
|
108
|
+
},
|
|
109
|
+
async execute(args, exec) {
|
|
110
|
+
try {
|
|
111
|
+
const db = await dbOf(exec);
|
|
112
|
+
const node = db.get(Number(args.id));
|
|
113
|
+
if (!node) return `Not found: ${args.id}`;
|
|
114
|
+
return JSON.stringify(
|
|
115
|
+
{
|
|
116
|
+
id: node.id,
|
|
117
|
+
payload: node.payload,
|
|
118
|
+
edges: node.edges,
|
|
119
|
+
incoming: listIncomingBusiness(db, node.id),
|
|
120
|
+
},
|
|
121
|
+
null,
|
|
122
|
+
2,
|
|
123
|
+
);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
return `ctx_read unavailable: ${err.message}`;
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
}),
|
|
129
|
+
),
|
|
130
|
+
"dsh-trivium.ctx_read",
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
ctx.effect(
|
|
134
|
+
() =>
|
|
135
|
+
ctx.tools.register(
|
|
136
|
+
textTool({
|
|
137
|
+
name: "ctx_remember",
|
|
138
|
+
description:
|
|
139
|
+
"Store a durable fact as an entity, preference, decision, or experience. Do not store secrets or one-off task instructions.",
|
|
140
|
+
parameters: {
|
|
141
|
+
text: { type: "string", required: true, description: "Fact to store." },
|
|
142
|
+
type: {
|
|
143
|
+
type: "string",
|
|
144
|
+
description: "entity | preference | decision | experience",
|
|
145
|
+
},
|
|
146
|
+
name: { type: "string", description: "Short name for entities." },
|
|
147
|
+
link_to: { type: "integer", description: "Optional existing node id to link." },
|
|
148
|
+
link_label: { type: "string", description: "Edge label when link_to is set." },
|
|
149
|
+
},
|
|
150
|
+
async execute(args, exec) {
|
|
151
|
+
try {
|
|
152
|
+
if (looksSecret(args.text)) return "Refused: looks like a secret.";
|
|
153
|
+
const type = ["entity", "preference", "decision", "experience"].includes(
|
|
154
|
+
args.type,
|
|
155
|
+
)
|
|
156
|
+
? args.type
|
|
157
|
+
: "preference";
|
|
158
|
+
const db = await dbOf(exec);
|
|
159
|
+
const incoming = {
|
|
160
|
+
type,
|
|
161
|
+
name: args.name || undefined,
|
|
162
|
+
text: args.text,
|
|
163
|
+
uri: `ctx://${type}/${Date.now()}`,
|
|
164
|
+
source: { sessionId: String(exec?.agent?.session?.header?.id || exec?.agent?.id || "") },
|
|
165
|
+
};
|
|
166
|
+
const existing = findMergeTarget(db, incoming);
|
|
167
|
+
const id =
|
|
168
|
+
existing != null ? mergeInto(db, existing, incoming) : insertNode(db, incoming);
|
|
169
|
+
if (args.link_to) {
|
|
170
|
+
const label = args.link_label || EDGE_LABELS.about;
|
|
171
|
+
ensureLink(db, id, Number(args.link_to), label);
|
|
172
|
+
}
|
|
173
|
+
return `Remembered id=${id} type=${type} db=${dbPathFor(cwdOf(exec))}`;
|
|
174
|
+
} catch (err) {
|
|
175
|
+
return `ctx_remember failed: ${err.message}`;
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
}),
|
|
179
|
+
),
|
|
180
|
+
"dsh-trivium.ctx_remember",
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
ctx.effect(
|
|
184
|
+
() =>
|
|
185
|
+
ctx.tools.register(
|
|
186
|
+
textTool({
|
|
187
|
+
name: "ctx_link",
|
|
188
|
+
description: "Create a directed edge between two memory node ids.",
|
|
189
|
+
parameters: {
|
|
190
|
+
from: { type: "integer", required: true, description: "Source node id." },
|
|
191
|
+
to: { type: "integer", required: true, description: "Target node id." },
|
|
192
|
+
label: { type: "string", description: "Edge label, default about." },
|
|
193
|
+
},
|
|
194
|
+
async execute(args, exec) {
|
|
195
|
+
try {
|
|
196
|
+
const db = await dbOf(exec);
|
|
197
|
+
const label = args.label || EDGE_LABELS.about;
|
|
198
|
+
ensureLink(db, Number(args.from), Number(args.to), label);
|
|
199
|
+
return `Linked ${args.from} -[${label}]-> ${args.to}`;
|
|
200
|
+
} catch (err) {
|
|
201
|
+
return `ctx_link failed: ${err.message}`;
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
}),
|
|
205
|
+
),
|
|
206
|
+
"dsh-trivium.ctx_link",
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
ctx.on("tools/pre-execute", async (exec, next) => {
|
|
210
|
+
if (!writeApprovalOf() || !WRITE_TOOLS.has(exec.name)) return next();
|
|
211
|
+
return {
|
|
212
|
+
kind: "ask",
|
|
213
|
+
reason: `dsh-trivium wants to persist ${exec.name}`,
|
|
214
|
+
};
|
|
215
|
+
});
|
|
216
|
+
}
|
package/lib/until.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** Display `until` is a label; `untilAt` is an absolute instant we can filter on. */
|
|
2
|
+
|
|
3
|
+
const WEEKDAYS = {
|
|
4
|
+
sunday: 0,
|
|
5
|
+
monday: 1,
|
|
6
|
+
tuesday: 2,
|
|
7
|
+
wednesday: 3,
|
|
8
|
+
thursday: 4,
|
|
9
|
+
friday: 5,
|
|
10
|
+
saturday: 6,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/** Pull a display `until` label out of a fact sentence. */
|
|
14
|
+
export function parseUntilFromText(text) {
|
|
15
|
+
const s = String(text || "");
|
|
16
|
+
if (/下周/.test(s)) return "下周";
|
|
17
|
+
const day = s.match(/\buntil\s+(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)\b/i);
|
|
18
|
+
if (day) return day[1];
|
|
19
|
+
const iso = s.match(/\b(\d{4}-\d{2}-\d{2})\b/);
|
|
20
|
+
if (iso) return iso[1];
|
|
21
|
+
const cn = s.match(/到?(\d{1,2}\s*月\d{1,2}\s*日|周五|周末)/);
|
|
22
|
+
if (cn) return cn[1];
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function resolveUntilAt(until, from = new Date()) {
|
|
27
|
+
const raw = String(until || "").trim();
|
|
28
|
+
if (!raw) return undefined;
|
|
29
|
+
const iso = raw.match(/^(\d{4}-\d{2}-\d{2})(?:[T\s]|$)/);
|
|
30
|
+
if (iso) {
|
|
31
|
+
const ms = Date.parse(`${iso[1]}T23:59:59.000Z`);
|
|
32
|
+
return Number.isFinite(ms) ? new Date(ms).toISOString() : undefined;
|
|
33
|
+
}
|
|
34
|
+
const fromDate = from instanceof Date ? from : new Date(from);
|
|
35
|
+
if (Number.isNaN(fromDate.getTime())) return undefined;
|
|
36
|
+
if (raw === "下周") {
|
|
37
|
+
return endOfDayUtc(addDays(fromDate, 7)).toISOString();
|
|
38
|
+
}
|
|
39
|
+
if (raw === "周五" || raw === "周末") {
|
|
40
|
+
const want = raw === "周末" ? 6 : 5;
|
|
41
|
+
return endOfDayUtc(nextWeekday(fromDate, want)).toISOString();
|
|
42
|
+
}
|
|
43
|
+
const day = raw.match(/^(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)$/i);
|
|
44
|
+
if (day) {
|
|
45
|
+
const want = WEEKDAYS[day[1].toLowerCase()];
|
|
46
|
+
return endOfDayUtc(nextWeekday(fromDate, want)).toISOString();
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function isStalePayload(payload, now = new Date()) {
|
|
52
|
+
const at = payload?.untilAt || resolveUntilAt(payload?.until, payload?.createdAt || now);
|
|
53
|
+
if (!at) return false;
|
|
54
|
+
const ms = Date.parse(at);
|
|
55
|
+
return Number.isFinite(ms) && ms < now.getTime();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function queryMentionsUntil(query, payload) {
|
|
59
|
+
const q = String(query || "").trim().toLowerCase();
|
|
60
|
+
if (!q) return false;
|
|
61
|
+
if (/下周|until|周五|周末|过期|到期|postpone|defer|friday|monday|tuesday|wednesday|thursday|saturday|sunday/.test(q)) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
const until = String(payload?.until || "").trim().toLowerCase();
|
|
65
|
+
return until.length >= 2 && q.includes(until);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function addDays(date, n) {
|
|
69
|
+
const next = new Date(date.getTime());
|
|
70
|
+
next.setUTCDate(next.getUTCDate() + n);
|
|
71
|
+
return next;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function endOfDayUtc(date) {
|
|
75
|
+
const next = new Date(date.getTime());
|
|
76
|
+
next.setUTCHours(23, 59, 59, 0);
|
|
77
|
+
return next;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function nextWeekday(from, weekday) {
|
|
81
|
+
const next = new Date(from.getTime());
|
|
82
|
+
const delta = (weekday - next.getUTCDay() + 7) % 7 || 7;
|
|
83
|
+
next.setUTCDate(next.getUTCDate() + delta);
|
|
84
|
+
return next;
|
|
85
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-trivium",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "In-process graph memory kernel for DeepSeek Harness, backed by TriviumDB.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "lib/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./lib/index.js",
|
|
10
|
+
"./client": "./lib/client.js",
|
|
11
|
+
"./package.json": "./package.json"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"lib",
|
|
15
|
+
"cordis.patch.yml",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"dsh": {
|
|
20
|
+
"bundle": {
|
|
21
|
+
"patch": "./cordis.patch.yml"
|
|
22
|
+
},
|
|
23
|
+
"client": {
|
|
24
|
+
"inject": [
|
|
25
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
26
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
27
|
+
],
|
|
28
|
+
"platform": "web"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"link-dsh": "node scripts/link-dsh.mjs",
|
|
33
|
+
"smoke-p0": "node scripts/smoke-p0.mjs",
|
|
34
|
+
"smoke-p1": "node scripts/smoke-p1.mjs",
|
|
35
|
+
"smoke-p2": "node scripts/p2-cases.mjs",
|
|
36
|
+
"smoke-p3": "node scripts/smoke-p3.mjs",
|
|
37
|
+
"smoke-p4": "node scripts/smoke-p4.mjs",
|
|
38
|
+
"smoke-p5": "node scripts/smoke-p5.mjs",
|
|
39
|
+
"verify-live-p2": "node scripts/verify-live-p2.mjs"
|
|
40
|
+
},
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": "^22.19.0 || >=24"
|
|
43
|
+
},
|
|
44
|
+
"peerDependencies": {
|
|
45
|
+
"@deepseek-ai/dsh-llm": "0.1.0-rc.6",
|
|
46
|
+
"@deepseek-ai/dsh-tools": "0.1.0-rc.6"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"triviumdb": "^0.7.4"
|
|
50
|
+
},
|
|
51
|
+
"keywords": [
|
|
52
|
+
"dsh-plugin",
|
|
53
|
+
"deepseek-harness",
|
|
54
|
+
"dsh",
|
|
55
|
+
"triviumdb",
|
|
56
|
+
"memory",
|
|
57
|
+
"graph",
|
|
58
|
+
"context"
|
|
59
|
+
],
|
|
60
|
+
"repository": {
|
|
61
|
+
"type": "git",
|
|
62
|
+
"url": "git+https://github.com/QWQcool/dsh-trivium.git"
|
|
63
|
+
},
|
|
64
|
+
"bugs": {
|
|
65
|
+
"url": "https://github.com/QWQcool/dsh-trivium/issues"
|
|
66
|
+
},
|
|
67
|
+
"homepage": "https://github.com/QWQcool/dsh-trivium#readme"
|
|
68
|
+
}
|