@hanhnd/agent-kit 1.0.30 → 1.0.32
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/__tests__/temp-dir.d.ts +4 -0
- package/dist/__tests__/temp-dir.js +20 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +9 -0
- package/dist/cli/memory.d.ts +1 -0
- package/dist/cli/memory.js +77 -0
- package/dist/kit-cli.d.ts +2 -0
- package/dist/kit-cli.js +16205 -0
- package/dist/kit-server.js +677 -187
- package/dist/memory/__tests__/digest-llama-local.test.d.ts +1 -0
- package/dist/memory/__tests__/digest-llama-local.test.js +33 -0
- package/dist/memory/__tests__/digest-pending.test.d.ts +1 -0
- package/dist/memory/__tests__/digest-pending.test.js +164 -0
- package/dist/memory/__tests__/digest-processor.test.d.ts +1 -0
- package/dist/memory/__tests__/digest-processor.test.js +34 -0
- package/dist/memory/__tests__/digest-worker.test.d.ts +1 -0
- package/dist/memory/__tests__/digest-worker.test.js +46 -0
- package/dist/memory/__tests__/embedder.test.d.ts +1 -0
- package/dist/memory/__tests__/embedder.test.js +14 -0
- package/dist/memory/__tests__/indexer.test.js +7 -12
- package/dist/memory/__tests__/lockfile.test.d.ts +1 -0
- package/dist/memory/__tests__/lockfile.test.js +81 -0
- package/dist/memory/__tests__/store.test.js +15 -3
- package/dist/memory/chunker.js +3 -5
- package/dist/memory/constants.d.ts +5 -0
- package/dist/memory/constants.js +12 -0
- package/dist/memory/digest/constants.d.ts +14 -0
- package/dist/memory/digest/constants.js +15 -0
- package/dist/memory/digest/files.d.ts +7 -0
- package/dist/memory/digest/files.js +45 -0
- package/dist/memory/digest/model-registry.d.ts +6 -0
- package/dist/memory/digest/model-registry.js +48 -0
- package/dist/memory/digest/processor.d.ts +7 -0
- package/dist/memory/digest/processor.js +171 -0
- package/dist/memory/digest/providers/llama-local.d.ts +7 -0
- package/dist/memory/digest/providers/llama-local.js +136 -0
- package/dist/memory/digest/types.d.ts +85 -0
- package/dist/memory/digest/types.js +1 -0
- package/dist/memory/digest/worker.d.ts +3 -0
- package/dist/memory/digest/worker.js +51 -0
- package/dist/memory/embedder.d.ts +2 -0
- package/dist/memory/embedder.js +12 -4
- package/dist/memory/index.d.ts +1 -1
- package/dist/memory/index.js +1 -1
- package/dist/memory/indexer.d.ts +5 -2
- package/dist/memory/indexer.js +15 -37
- package/dist/memory/store.js +6 -18
- package/dist/memory/types.d.ts +2 -1
- package/dist/memory/types.js +1 -9
- package/dist/tools/__tests__/cli-digest-pending.test.d.ts +1 -0
- package/dist/tools/__tests__/cli-digest-pending.test.js +51 -0
- package/dist/tools/__tests__/cli-memory.test.d.ts +1 -0
- package/dist/tools/__tests__/cli-memory.test.js +22 -0
- package/dist/tools/__tests__/digest-init.test.d.ts +1 -0
- package/dist/tools/__tests__/digest-init.test.js +32 -0
- package/dist/tools/__tests__/memory.test.js +28 -6
- package/dist/tools/agent.js +2 -13
- package/dist/tools/config.d.ts +4 -4
- package/dist/tools/config.js +21 -13
- package/dist/tools/core.js +1 -3
- package/dist/tools/handoffs.js +1 -2
- package/dist/tools/integration.js +2 -8
- package/dist/tools/memory.d.ts +1 -1
- package/dist/tools/memory.js +29 -3
- package/dist/tools/security.js +2 -6
- package/dist/utils/args.d.ts +7 -0
- package/dist/utils/args.js +31 -0
- package/dist/utils/async.d.ts +1 -0
- package/dist/utils/async.js +15 -0
- package/dist/utils/credentials.d.ts +3 -3
- package/dist/utils/credentials.js +5 -7
- package/dist/utils/files.d.ts +8 -0
- package/dist/utils/files.js +77 -0
- package/dist/utils/hash.d.ts +1 -0
- package/dist/utils/hash.js +4 -0
- package/dist/utils/json.d.ts +2 -0
- package/dist/utils/json.js +13 -0
- package/dist/utils/parser.js +2 -6
- package/dist/utils/paths.d.ts +4 -0
- package/dist/utils/paths.js +6 -0
- package/package.json +7 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as assert from 'node:assert/strict';
|
|
2
|
+
import { describe, test } from 'node:test';
|
|
3
|
+
import { createLlamaLocalDigestProvider, sanitizeConversationDigestMarkdown, trimConversationExport, } from '../digest/providers/llama-local.js';
|
|
4
|
+
const INPUT = {
|
|
5
|
+
sourcePath: '.agent-kit/wiki/archive/conversations/conv_2026-05-19T00-45-17-697Z.md',
|
|
6
|
+
contentHash: 'tiny',
|
|
7
|
+
content: '**User:** use local digest generation for archived conversations',
|
|
8
|
+
};
|
|
9
|
+
describe('createLlamaLocalDigestProvider', () => {
|
|
10
|
+
test('strips code fences without rewriting model output', () => {
|
|
11
|
+
const markdown = sanitizeConversationDigestMarkdown('```md\n# Notes\n\n**Summary**\n\nUse local digesting.\n```');
|
|
12
|
+
assert.match(markdown, /^# Notes/);
|
|
13
|
+
assert.match(markdown, /\*\*Summary\*\*/);
|
|
14
|
+
assert.doesNotMatch(markdown, /```/);
|
|
15
|
+
});
|
|
16
|
+
test('trims conversation exports from the end without parsing turns', () => {
|
|
17
|
+
const trimmed = trimConversationExport('first decision\nsecond decision\nfinal decision', 14);
|
|
18
|
+
assert.match(trimmed, /omitted/);
|
|
19
|
+
assert.doesNotMatch(trimmed, /first decision/);
|
|
20
|
+
assert.match(trimmed, /final decision/);
|
|
21
|
+
});
|
|
22
|
+
test('runs an opt-in smoke test against a real model', { skip: !process.env.AGENT_KIT_DIGEST_MODEL_ID }, async () => {
|
|
23
|
+
const modelId = process.env.AGENT_KIT_DIGEST_MODEL_ID;
|
|
24
|
+
const provider = await createLlamaLocalDigestProvider(modelId);
|
|
25
|
+
const output = await provider.generateDigestMarkdown(INPUT, {
|
|
26
|
+
modelId,
|
|
27
|
+
maxInputChars: 1000,
|
|
28
|
+
timeoutMs: 120_000,
|
|
29
|
+
});
|
|
30
|
+
assert.match(output, /^# Conversation Digest:/);
|
|
31
|
+
assert.match(output, /Status: provisional/);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import * as assert from 'node:assert/strict';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { afterEach, describe, test } from 'node:test';
|
|
5
|
+
import { createTempDirTracker } from '../../__tests__/temp-dir.js';
|
|
6
|
+
import { writeConversationDigestSettings } from '../digest/files.js';
|
|
7
|
+
import { digestPendingConversations } from '../digest/processor.js';
|
|
8
|
+
const tempDirs = createTempDirTracker();
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
tempDirs.cleanup();
|
|
11
|
+
});
|
|
12
|
+
function writeInitState(workspace, modelId = 'qwen2.5-1.5b-instruct-q4') {
|
|
13
|
+
writeConversationDigestSettings(workspace, {
|
|
14
|
+
enabled: true,
|
|
15
|
+
initialized: true,
|
|
16
|
+
modelId,
|
|
17
|
+
initializedAt: new Date().toISOString(),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function writeConvFile(workspace, name, content) {
|
|
21
|
+
const rawDir = path.join(workspace, '.agent-kit', 'wiki', 'raw');
|
|
22
|
+
fs.mkdirSync(rawDir, { recursive: true });
|
|
23
|
+
const filePath = path.join(rawDir, name);
|
|
24
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
25
|
+
return filePath;
|
|
26
|
+
}
|
|
27
|
+
function stubDigest(result = {}) {
|
|
28
|
+
return async () => ({
|
|
29
|
+
markdown: 'out.md',
|
|
30
|
+
status: 'provisional',
|
|
31
|
+
contentHash: 'abc123',
|
|
32
|
+
skipped: false,
|
|
33
|
+
indexed: false,
|
|
34
|
+
...result,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
describe('digestPendingConversations', () => {
|
|
38
|
+
test('C13: no init state → noop not-initialized', async () => {
|
|
39
|
+
const workspace = tempDirs.makeTempDir('pending-c13-');
|
|
40
|
+
const result = await digestPendingConversations({ workspaceRoot: workspace });
|
|
41
|
+
assert.equal(result.ok, true);
|
|
42
|
+
assert.equal(result.initialized, false);
|
|
43
|
+
assert.equal(result.action, 'noop');
|
|
44
|
+
if (result.action === 'noop')
|
|
45
|
+
assert.equal(result.reason, 'not-initialized');
|
|
46
|
+
});
|
|
47
|
+
test('C14: init state, live lockfile → noop locked', async () => {
|
|
48
|
+
const workspace = tempDirs.makeTempDir('pending-c14-');
|
|
49
|
+
writeInitState(workspace);
|
|
50
|
+
const lockDir = path.join(workspace, '.agent-kit');
|
|
51
|
+
fs.mkdirSync(lockDir, { recursive: true });
|
|
52
|
+
fs.writeFileSync(path.join(lockDir, 'digest-worker.lock'), JSON.stringify({ pid: process.pid }));
|
|
53
|
+
const result = await digestPendingConversations({ workspaceRoot: workspace });
|
|
54
|
+
assert.equal(result.ok, true);
|
|
55
|
+
assert.equal(result.initialized, true);
|
|
56
|
+
assert.equal(result.action, 'noop');
|
|
57
|
+
if (result.action === 'noop')
|
|
58
|
+
assert.equal(result.reason, 'locked');
|
|
59
|
+
});
|
|
60
|
+
test('C15: init state, no lockfile, no conv files → noop no-pending, lock released', async () => {
|
|
61
|
+
const workspace = tempDirs.makeTempDir('pending-c15-');
|
|
62
|
+
writeInitState(workspace);
|
|
63
|
+
const result = await digestPendingConversations({ workspaceRoot: workspace });
|
|
64
|
+
assert.equal(result.ok, true);
|
|
65
|
+
assert.equal(result.action, 'noop');
|
|
66
|
+
if (result.action === 'noop')
|
|
67
|
+
assert.equal(result.reason, 'no-pending');
|
|
68
|
+
assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'digest-worker.lock')), false);
|
|
69
|
+
});
|
|
70
|
+
test('C19: wiki/raw dir does not exist → noop no-pending', async () => {
|
|
71
|
+
const workspace = tempDirs.makeTempDir('pending-c19-');
|
|
72
|
+
writeInitState(workspace);
|
|
73
|
+
const result = await digestPendingConversations({ workspaceRoot: workspace });
|
|
74
|
+
assert.equal(result.action, 'noop');
|
|
75
|
+
if (result.action === 'noop')
|
|
76
|
+
assert.equal(result.reason, 'no-pending');
|
|
77
|
+
});
|
|
78
|
+
test('C16: one undigested conv file → digested count:1, lock released', async () => {
|
|
79
|
+
const workspace = tempDirs.makeTempDir('pending-c16-');
|
|
80
|
+
writeInitState(workspace);
|
|
81
|
+
writeConvFile(workspace, 'conv_2026-01-01T00-00-00-001Z.md', '**User:** hello\n\n**Assistant:** hi\n');
|
|
82
|
+
const result = await digestPendingConversations({
|
|
83
|
+
workspaceRoot: workspace,
|
|
84
|
+
digestFn: stubDigest(),
|
|
85
|
+
});
|
|
86
|
+
assert.equal(result.ok, true);
|
|
87
|
+
assert.equal(result.action, 'digested');
|
|
88
|
+
if (result.action === 'digested') {
|
|
89
|
+
assert.equal(result.count, 1);
|
|
90
|
+
assert.equal(result.errors, 0);
|
|
91
|
+
}
|
|
92
|
+
assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'digest-worker.lock')), false);
|
|
93
|
+
});
|
|
94
|
+
test('C17: digestFn throws for one file → errors:1, lock released', async () => {
|
|
95
|
+
const workspace = tempDirs.makeTempDir('pending-c17-');
|
|
96
|
+
writeInitState(workspace);
|
|
97
|
+
writeConvFile(workspace, 'conv_2026-01-01T00-00-00-001Z.md', '**User:** hello\n\n**Assistant:** hi\n');
|
|
98
|
+
const throwingFn = async () => {
|
|
99
|
+
throw new Error('model error');
|
|
100
|
+
};
|
|
101
|
+
const result = await digestPendingConversations({
|
|
102
|
+
workspaceRoot: workspace,
|
|
103
|
+
digestFn: throwingFn,
|
|
104
|
+
});
|
|
105
|
+
assert.equal(result.ok, true);
|
|
106
|
+
assert.equal(result.action, 'digested');
|
|
107
|
+
if (result.action === 'digested') {
|
|
108
|
+
assert.equal(result.errors, 1);
|
|
109
|
+
assert.equal(result.count, 0);
|
|
110
|
+
}
|
|
111
|
+
assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'digest-worker.lock')), false);
|
|
112
|
+
});
|
|
113
|
+
test('C18: stale lockfile (dead PID) → reclaims lock and returns (no-pending since no conv files)', async () => {
|
|
114
|
+
const workspace = tempDirs.makeTempDir('pending-c18-');
|
|
115
|
+
writeInitState(workspace);
|
|
116
|
+
const lockDir = path.join(workspace, '.agent-kit');
|
|
117
|
+
fs.mkdirSync(lockDir, { recursive: true });
|
|
118
|
+
fs.writeFileSync(path.join(lockDir, 'digest-worker.lock'), JSON.stringify({ pid: 999999999 }));
|
|
119
|
+
const result = await digestPendingConversations({ workspaceRoot: workspace });
|
|
120
|
+
assert.ok(result.ok);
|
|
121
|
+
if (result.action === 'noop')
|
|
122
|
+
assert.notEqual(result.reason, 'locked');
|
|
123
|
+
assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'digest-worker.lock')), false);
|
|
124
|
+
});
|
|
125
|
+
test('C20: 3 undigested conv files → digestFn called 3 times, count+skipped+errors=3', async () => {
|
|
126
|
+
const workspace = tempDirs.makeTempDir('pending-c20-');
|
|
127
|
+
writeInitState(workspace);
|
|
128
|
+
writeConvFile(workspace, 'conv_2026-01-01T00-00-00-001Z.md', '**User:** a\n\n**Assistant:** b\n');
|
|
129
|
+
writeConvFile(workspace, 'conv_2026-01-01T00-00-00-002Z.md', '**User:** c\n\n**Assistant:** d\n');
|
|
130
|
+
writeConvFile(workspace, 'conv_2026-01-01T00-00-00-003Z.md', '**User:** e\n\n**Assistant:** f\n');
|
|
131
|
+
let callCount = 0;
|
|
132
|
+
const countingFn = async () => {
|
|
133
|
+
callCount++;
|
|
134
|
+
return {
|
|
135
|
+
markdown: 'out.md',
|
|
136
|
+
status: 'provisional',
|
|
137
|
+
contentHash: 'abc',
|
|
138
|
+
skipped: false,
|
|
139
|
+
indexed: false,
|
|
140
|
+
};
|
|
141
|
+
};
|
|
142
|
+
const result = await digestPendingConversations({
|
|
143
|
+
workspaceRoot: workspace,
|
|
144
|
+
digestFn: countingFn,
|
|
145
|
+
});
|
|
146
|
+
assert.equal(callCount, 3);
|
|
147
|
+
assert.equal(result.action, 'digested');
|
|
148
|
+
if (result.action === 'digested') {
|
|
149
|
+
assert.equal(result.count + result.skipped + result.errors, 3);
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
test('C21: outer error (fs throws unexpected) → returns ok:false, releases lock', async () => {
|
|
153
|
+
const workspace = tempDirs.makeTempDir('pending-c21-');
|
|
154
|
+
writeInitState(workspace);
|
|
155
|
+
// Create wiki/raw as a file (not a dir) so readdirSync throws ENOTDIR
|
|
156
|
+
const wikiDir = path.join(workspace, '.agent-kit', 'wiki');
|
|
157
|
+
fs.mkdirSync(wikiDir, { recursive: true });
|
|
158
|
+
fs.writeFileSync(path.join(wikiDir, 'raw'), 'not-a-directory', 'utf8');
|
|
159
|
+
const result = await digestPendingConversations({ workspaceRoot: workspace });
|
|
160
|
+
assert.equal(result.ok, false);
|
|
161
|
+
assert.equal(result.action, 'error');
|
|
162
|
+
assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'digest-worker.lock')), false);
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import * as assert from 'node:assert/strict';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { afterEach, describe, test } from 'node:test';
|
|
5
|
+
import { createTempDirTracker } from '../../__tests__/temp-dir.js';
|
|
6
|
+
import { DEFAULT_DIGEST_MODEL_ID } from '../digest/constants.js';
|
|
7
|
+
import { defaultProvisionalDigestDir, provisionalDigestPath, readConversationDigestInput } from '../digest/files.js';
|
|
8
|
+
import { digestConversationFile } from '../digest/processor.js';
|
|
9
|
+
const tempDirs = createTempDirTracker();
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
tempDirs.cleanup();
|
|
12
|
+
});
|
|
13
|
+
describe('digestConversationFile', () => {
|
|
14
|
+
test('returns existing content-hash named provisional digest without loading model', async () => {
|
|
15
|
+
const workspace = tempDirs.makeTempDir('digest-processor-');
|
|
16
|
+
const inputPath = path.join(workspace, '.agent-kit', 'wiki', 'archive', 'conversations', 'conv.md');
|
|
17
|
+
fs.mkdirSync(path.dirname(inputPath), { recursive: true });
|
|
18
|
+
fs.writeFileSync(inputPath, '**User:** remember this decision', 'utf8');
|
|
19
|
+
const input = readConversationDigestInput(workspace, inputPath);
|
|
20
|
+
const outDir = defaultProvisionalDigestDir(workspace);
|
|
21
|
+
const markdownPath = provisionalDigestPath(outDir, input);
|
|
22
|
+
fs.mkdirSync(path.dirname(markdownPath), { recursive: true });
|
|
23
|
+
fs.writeFileSync(markdownPath, '# Conversation Digest: conv\n', 'utf8');
|
|
24
|
+
const result = await digestConversationFile({
|
|
25
|
+
workspaceRoot: workspace,
|
|
26
|
+
inputPath,
|
|
27
|
+
modelId: DEFAULT_DIGEST_MODEL_ID,
|
|
28
|
+
});
|
|
29
|
+
assert.equal(result.skipped, true);
|
|
30
|
+
assert.equal(result.markdown, markdownPath);
|
|
31
|
+
assert.equal(result.contentHash, input.contentHash);
|
|
32
|
+
assert.match(path.basename(result.markdown), /^[a-f0-9]{16}-conv\.md$/);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as assert from 'node:assert/strict';
|
|
2
|
+
import { describe, test } from 'node:test';
|
|
3
|
+
import { parseDigestWorkerSuccess } from '../digest/worker.js';
|
|
4
|
+
import { DIGEST_WORKER_RESULT_PREFIX } from '../digest/constants.js';
|
|
5
|
+
function workerResultLine(result) {
|
|
6
|
+
return DIGEST_WORKER_RESULT_PREFIX + JSON.stringify(result);
|
|
7
|
+
}
|
|
8
|
+
describe('parseDigestWorkerSuccess', () => {
|
|
9
|
+
test('accepts a structured provisional digest worker line', () => {
|
|
10
|
+
const stdout = [
|
|
11
|
+
'[memory-kit] model-load: 612ms',
|
|
12
|
+
workerResultLine({
|
|
13
|
+
markdown: '/tmp/conversation-digests/abc-conv.md',
|
|
14
|
+
status: 'provisional',
|
|
15
|
+
contentHash: 'abc',
|
|
16
|
+
indexed: true,
|
|
17
|
+
skipped: false,
|
|
18
|
+
}),
|
|
19
|
+
].join('\n');
|
|
20
|
+
assert.equal(parseDigestWorkerSuccess(stdout), true);
|
|
21
|
+
});
|
|
22
|
+
test('rejects stdout that only mentions markdown as plain text', () => {
|
|
23
|
+
const stdout = 'The model produced markdown but no digest result envelope.';
|
|
24
|
+
assert.equal(parseDigestWorkerSuccess(stdout), false);
|
|
25
|
+
});
|
|
26
|
+
test('rejects unprefixed JSON objects', () => {
|
|
27
|
+
const stdout = [
|
|
28
|
+
JSON.stringify({ markdown: '/tmp/file.md', status: 'draft' }),
|
|
29
|
+
JSON.stringify({ markdown: '/tmp/file.md', status: 'provisional' }),
|
|
30
|
+
].join('\n');
|
|
31
|
+
assert.equal(parseDigestWorkerSuccess(stdout), false);
|
|
32
|
+
});
|
|
33
|
+
test('uses the last valid digest worker line after noisy output', () => {
|
|
34
|
+
const stdout = [
|
|
35
|
+
JSON.stringify({ event: 'model-load', markdown: false }),
|
|
36
|
+
'Metal cleanup warning',
|
|
37
|
+
workerResultLine({ markdown: '/tmp/final.md', status: 'provisional' }),
|
|
38
|
+
'trailing non-json output',
|
|
39
|
+
].join('\n');
|
|
40
|
+
assert.equal(parseDigestWorkerSuccess(stdout), true);
|
|
41
|
+
});
|
|
42
|
+
test('rejects malformed digest worker lines', () => {
|
|
43
|
+
const stdout = ['[memory-kit] model-load: 612ms', DIGEST_WORKER_RESULT_PREFIX + '{"markdown":'].join('\n');
|
|
44
|
+
assert.equal(parseDigestWorkerSuccess(stdout), false);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as assert from 'node:assert/strict';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { describe, test } from 'node:test';
|
|
5
|
+
import { DEFAULT_MODEL_CACHE_DIR, FASTEMBED_CACHE_DIR } from '../embedder.js';
|
|
6
|
+
import { AGENT_KIT_HOME, CREDENTIALS_FILE } from '../../utils/paths.js';
|
|
7
|
+
describe('Embedder cache paths', () => {
|
|
8
|
+
test('uses global Agent Kit home for user-level files', () => {
|
|
9
|
+
assert.equal(AGENT_KIT_HOME, path.join(os.homedir(), '.agent-kit'));
|
|
10
|
+
assert.equal(CREDENTIALS_FILE, path.join(AGENT_KIT_HOME, 'credentials'));
|
|
11
|
+
assert.equal(DEFAULT_MODEL_CACHE_DIR, path.join(AGENT_KIT_HOME, 'cache', 'models'));
|
|
12
|
+
assert.equal(FASTEMBED_CACHE_DIR, path.join(DEFAULT_MODEL_CACHE_DIR, 'fastembed'));
|
|
13
|
+
});
|
|
14
|
+
});
|
|
@@ -7,16 +7,12 @@ import * as path from 'node:path';
|
|
|
7
7
|
import { after, before, describe, test } from 'node:test';
|
|
8
8
|
import { MemoryIndexer } from '../indexer.js';
|
|
9
9
|
import { MemoryStore } from '../store.js';
|
|
10
|
-
// Stub embedder — returns deterministic non-zero vectors without loading any model
|
|
11
10
|
class StubEmbedder {
|
|
12
11
|
async embed(texts) {
|
|
13
12
|
return texts.map(() => new Float32Array(384).fill(0.05));
|
|
14
13
|
}
|
|
15
|
-
|
|
16
|
-
return
|
|
17
|
-
}
|
|
18
|
-
isReady() {
|
|
19
|
-
return true;
|
|
14
|
+
initialize() {
|
|
15
|
+
return Promise.resolve();
|
|
20
16
|
}
|
|
21
17
|
}
|
|
22
18
|
function makeConfig(wikiDir) {
|
|
@@ -140,7 +136,8 @@ describe('MemoryIndexer', () => {
|
|
|
140
136
|
const compiledDir = path.join(testCfg.wikiDir, 'compiled');
|
|
141
137
|
try {
|
|
142
138
|
fs.mkdirSync(compiledDir, { recursive: true });
|
|
143
|
-
testStore.upsert([
|
|
139
|
+
testStore.upsert([
|
|
140
|
+
{
|
|
144
141
|
id: 'stale-daily-file-0001',
|
|
145
142
|
source: '2026-05-18.md',
|
|
146
143
|
heading: 'Stale',
|
|
@@ -148,7 +145,8 @@ describe('MemoryIndexer', () => {
|
|
|
148
145
|
content: 'pre migration content',
|
|
149
146
|
lineStart: 1,
|
|
150
147
|
lineEnd: 2,
|
|
151
|
-
}
|
|
148
|
+
},
|
|
149
|
+
], [new Float32Array(384)]);
|
|
152
150
|
assert.ok(testStore.hashesBySource('2026-05-18.md').size > 0);
|
|
153
151
|
const stats = await testIndexer.indexDirectory(compiledDir, {
|
|
154
152
|
relativeBase: testCfg.wikiDir,
|
|
@@ -257,10 +255,7 @@ describe('MemoryIndexer', () => {
|
|
|
257
255
|
fs.writeFileSync(secondPath, secondContent, 'utf8');
|
|
258
256
|
const results = await testIndexer.search('duplicate source query', 2);
|
|
259
257
|
assert.equal(results.length, 2);
|
|
260
|
-
assert.deepEqual(results.map((result) => result.chunk.source), [
|
|
261
|
-
'compiled/entities/first.md',
|
|
262
|
-
'compiled/entities/second.md',
|
|
263
|
-
]);
|
|
258
|
+
assert.deepEqual(results.map((result) => result.chunk.source), ['compiled/entities/first.md', 'compiled/entities/second.md']);
|
|
264
259
|
assert.equal(results[0].chunk.content, firstContent);
|
|
265
260
|
assert.equal(results[1].chunk.content, secondContent);
|
|
266
261
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import * as assert from 'node:assert/strict';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { afterEach, describe, test } from 'node:test';
|
|
5
|
+
import { createTempDirTracker } from '../../__tests__/temp-dir.js';
|
|
6
|
+
import { DIGEST_LOCKFILE_REL_PATH } from '../digest/constants.js';
|
|
7
|
+
import { releaseLock, tryAcquireProcessLock } from '../../utils/files.js';
|
|
8
|
+
const tempDirs = createTempDirTracker();
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
tempDirs.cleanup();
|
|
11
|
+
});
|
|
12
|
+
function lockPath(workspace) {
|
|
13
|
+
return path.join(workspace, DIGEST_LOCKFILE_REL_PATH);
|
|
14
|
+
}
|
|
15
|
+
describe('tryAcquireProcessLock', () => {
|
|
16
|
+
test('C1: no lockfile → creates it with current PID and returns true', () => {
|
|
17
|
+
const workspace = tempDirs.makeTempDir('lockfile-c1-');
|
|
18
|
+
const acquired = tryAcquireProcessLock(lockPath(workspace));
|
|
19
|
+
assert.equal(acquired, true);
|
|
20
|
+
const lockFile = path.join(workspace, '.agent-kit', 'digest-worker.lock');
|
|
21
|
+
const content = JSON.parse(fs.readFileSync(lockFile, 'utf8'));
|
|
22
|
+
assert.equal(content.pid, process.pid);
|
|
23
|
+
});
|
|
24
|
+
test('C2: lockfile with live foreign PID → returns false without modifying file', () => {
|
|
25
|
+
const workspace = tempDirs.makeTempDir('lockfile-c2-');
|
|
26
|
+
const lockDir = path.join(workspace, '.agent-kit');
|
|
27
|
+
const lockFile = path.join(lockDir, 'digest-worker.lock');
|
|
28
|
+
fs.mkdirSync(lockDir, { recursive: true });
|
|
29
|
+
// Use process.pid as "live" pid (current process is definitely alive)
|
|
30
|
+
const livePid = process.pid;
|
|
31
|
+
fs.writeFileSync(lockFile, JSON.stringify({ pid: livePid }), 'utf8');
|
|
32
|
+
const originalContent = fs.readFileSync(lockFile, 'utf8');
|
|
33
|
+
const acquired = tryAcquireProcessLock(lockPath(workspace));
|
|
34
|
+
assert.equal(acquired, false);
|
|
35
|
+
// File must be unchanged
|
|
36
|
+
assert.equal(fs.readFileSync(lockFile, 'utf8'), originalContent);
|
|
37
|
+
});
|
|
38
|
+
test('C3: lockfile with dead PID (ESRCH) → deletes stale lock, creates new one, returns true', () => {
|
|
39
|
+
const workspace = tempDirs.makeTempDir('lockfile-c3-');
|
|
40
|
+
const lockDir = path.join(workspace, '.agent-kit');
|
|
41
|
+
const lockFile = path.join(lockDir, 'digest-worker.lock');
|
|
42
|
+
fs.mkdirSync(lockDir, { recursive: true });
|
|
43
|
+
// PID 999999999 is virtually guaranteed to be dead
|
|
44
|
+
fs.writeFileSync(lockFile, JSON.stringify({ pid: 999999999 }), 'utf8');
|
|
45
|
+
const acquired = tryAcquireProcessLock(lockPath(workspace));
|
|
46
|
+
assert.equal(acquired, true);
|
|
47
|
+
const content = JSON.parse(fs.readFileSync(lockFile, 'utf8'));
|
|
48
|
+
assert.equal(content.pid, process.pid);
|
|
49
|
+
});
|
|
50
|
+
test('C4: concurrent callers — only one returns true (wx atomicity)', async () => {
|
|
51
|
+
const workspace = tempDirs.makeTempDir('lockfile-c4-');
|
|
52
|
+
// Call twice in the same tick; first wins, second sees EEXIST with live PID.
|
|
53
|
+
const r1 = tryAcquireProcessLock(lockPath(workspace));
|
|
54
|
+
const r2 = tryAcquireProcessLock(lockPath(workspace));
|
|
55
|
+
assert.equal(r1, true);
|
|
56
|
+
assert.equal(r2, false);
|
|
57
|
+
});
|
|
58
|
+
test('stale unparseable lockfile → treated as stale, returns true', () => {
|
|
59
|
+
const workspace = tempDirs.makeTempDir('lockfile-stale-');
|
|
60
|
+
const lockDir = path.join(workspace, '.agent-kit');
|
|
61
|
+
const lockFile = path.join(lockDir, 'digest-worker.lock');
|
|
62
|
+
fs.mkdirSync(lockDir, { recursive: true });
|
|
63
|
+
fs.writeFileSync(lockFile, 'not-json', 'utf8');
|
|
64
|
+
const acquired = tryAcquireProcessLock(lockPath(workspace));
|
|
65
|
+
assert.equal(acquired, true);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('releaseLock', () => {
|
|
69
|
+
test('C5: lockfile exists → deletes it', () => {
|
|
70
|
+
const workspace = tempDirs.makeTempDir('lockfile-c5-');
|
|
71
|
+
assert.equal(tryAcquireProcessLock(lockPath(workspace)), true);
|
|
72
|
+
const lockFile = path.join(workspace, '.agent-kit', 'digest-worker.lock');
|
|
73
|
+
assert.equal(fs.existsSync(lockFile), true);
|
|
74
|
+
releaseLock(lockPath(workspace));
|
|
75
|
+
assert.equal(fs.existsSync(lockFile), false);
|
|
76
|
+
});
|
|
77
|
+
test('C6: no lockfile → does not throw', () => {
|
|
78
|
+
const workspace = tempDirs.makeTempDir('lockfile-c6-');
|
|
79
|
+
assert.doesNotThrow(() => releaseLock(lockPath(workspace)));
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -54,8 +54,16 @@ describe('MemoryStore', () => {
|
|
|
54
54
|
assert.ok(hashes.has('upsert-test-0001'));
|
|
55
55
|
});
|
|
56
56
|
test('deleteBySource removes all chunks for that source', () => {
|
|
57
|
-
const c1 = makeChunk({
|
|
58
|
-
|
|
57
|
+
const c1 = makeChunk({
|
|
58
|
+
id: 'del-src-0001',
|
|
59
|
+
source: 'deleteme.md',
|
|
60
|
+
content: 'delete source chunk 1',
|
|
61
|
+
});
|
|
62
|
+
const c2 = makeChunk({
|
|
63
|
+
id: 'del-src-0002',
|
|
64
|
+
source: 'deleteme.md',
|
|
65
|
+
content: 'delete source chunk 2',
|
|
66
|
+
});
|
|
59
67
|
store.upsert([c1, c2], [new Float32Array(384), new Float32Array(384)]);
|
|
60
68
|
store.deleteBySource('deleteme.md');
|
|
61
69
|
const hashes = store.hashesBySource('deleteme.md');
|
|
@@ -124,7 +132,11 @@ describe('MemoryStore', () => {
|
|
|
124
132
|
assert.equal(results[0].id, 'partial-found-0001');
|
|
125
133
|
});
|
|
126
134
|
test('indexedSources includes source after upsert', () => {
|
|
127
|
-
const chunk = makeChunk({
|
|
135
|
+
const chunk = makeChunk({
|
|
136
|
+
id: 'indexed-src-0001',
|
|
137
|
+
source: 'indexed-source.md',
|
|
138
|
+
content: 'indexed',
|
|
139
|
+
});
|
|
128
140
|
store.upsert([chunk], [new Float32Array(384)]);
|
|
129
141
|
const sources = store.indexedSources();
|
|
130
142
|
assert.ok(sources.includes('indexed-source.md'), `Expected 'indexed-source.md' in ${sources.join(', ')}`);
|
package/dist/memory/chunker.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { sha256Hex } from '../utils/hash.js';
|
|
2
2
|
export function computeChunkId(content) {
|
|
3
|
-
return
|
|
3
|
+
return sha256Hex(content).slice(0, 16);
|
|
4
4
|
}
|
|
5
5
|
export function cleanContentForEmbedding(text) {
|
|
6
6
|
return text.replace(/<!--[\s\S]*?-->/g, '');
|
|
@@ -116,9 +116,7 @@ export function chunkMarkdown(text, source, config) {
|
|
|
116
116
|
const sectionText = section.lines.map((l) => l.text).join('\n');
|
|
117
117
|
if (!sectionText.trim())
|
|
118
118
|
continue;
|
|
119
|
-
const fullText = prevOverlapLines.length > 0
|
|
120
|
-
? `${prevOverlapLines.join('\n')}\n${sectionText}`
|
|
121
|
-
: sectionText;
|
|
119
|
+
const fullText = prevOverlapLines.length > 0 ? `${prevOverlapLines.join('\n')}\n${sectionText}` : sectionText;
|
|
122
120
|
const parts = splitSection(fullText, chunkSize);
|
|
123
121
|
for (let pi = 0; pi < parts.length; pi++) {
|
|
124
122
|
const part = parts[pi];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const DEFAULT_MEMORY_CONFIG = {
|
|
2
|
+
enabled: false,
|
|
3
|
+
wikiDir: '',
|
|
4
|
+
topK: 5,
|
|
5
|
+
chunkSize: 1500,
|
|
6
|
+
overlapLines: 2,
|
|
7
|
+
embeddingModel: 'Xenova/bge-small-en-v1.5',
|
|
8
|
+
vectorDimension: 384,
|
|
9
|
+
};
|
|
10
|
+
export const LOCK_RETRY_MS = 50;
|
|
11
|
+
export const LOCK_TIMEOUT_MS = 500;
|
|
12
|
+
export const RRF_K = 60;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare const DEFAULT_DIGEST_MODEL_ID = "qwen2.5-1.5b-instruct-q4";
|
|
2
|
+
export declare const DEFAULT_DIGEST_MAX_INPUT_CHARS = 3000;
|
|
3
|
+
export declare const DEFAULT_DIGEST_TIMEOUT_MS = 120000;
|
|
4
|
+
export declare const DIGEST_TIMEOUT_GRACE_MS = 5000;
|
|
5
|
+
export declare const DIGEST_WORKER_FLAG = "__agent-kit-digest-worker";
|
|
6
|
+
export declare const DIGEST_WORKER_RESULT_PREFIX = "AGENT_KIT_DIGEST_RESULT ";
|
|
7
|
+
export declare const KIT_DIR = ".agent-kit";
|
|
8
|
+
export declare const WIKI_DIR: string;
|
|
9
|
+
export declare const WIKI_RAW_DIR: string;
|
|
10
|
+
export declare const PROVISIONAL_DIGEST_DIR: string;
|
|
11
|
+
export declare const DIGEST_LOCKFILE_REL_PATH: string;
|
|
12
|
+
export declare const LLAMA_CONTEXT_SIZE = 4096;
|
|
13
|
+
export declare const LLAMA_MAX_GENERATED_TOKENS = 512;
|
|
14
|
+
export declare const LLAMA_TEMPERATURE = 0.1;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
export const DEFAULT_DIGEST_MODEL_ID = 'qwen2.5-1.5b-instruct-q4';
|
|
3
|
+
export const DEFAULT_DIGEST_MAX_INPUT_CHARS = 3000;
|
|
4
|
+
export const DEFAULT_DIGEST_TIMEOUT_MS = 120_000;
|
|
5
|
+
export const DIGEST_TIMEOUT_GRACE_MS = 5_000;
|
|
6
|
+
export const DIGEST_WORKER_FLAG = '__agent-kit-digest-worker';
|
|
7
|
+
export const DIGEST_WORKER_RESULT_PREFIX = 'AGENT_KIT_DIGEST_RESULT ';
|
|
8
|
+
export const KIT_DIR = '.agent-kit';
|
|
9
|
+
export const WIKI_DIR = path.join(KIT_DIR, 'wiki');
|
|
10
|
+
export const WIKI_RAW_DIR = path.join(WIKI_DIR, 'raw');
|
|
11
|
+
export const PROVISIONAL_DIGEST_DIR = path.join(WIKI_DIR, 'compiled', 'provisional', 'conversation-digests');
|
|
12
|
+
export const DIGEST_LOCKFILE_REL_PATH = path.join(KIT_DIR, 'digest-worker.lock');
|
|
13
|
+
export const LLAMA_CONTEXT_SIZE = 4096;
|
|
14
|
+
export const LLAMA_MAX_GENERATED_TOKENS = 512;
|
|
15
|
+
export const LLAMA_TEMPERATURE = 0.1;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ConversationDigestInput, ConversationDigestSettings } from './types.js';
|
|
2
|
+
export declare function defaultProvisionalDigestDir(workspaceRoot: string): string;
|
|
3
|
+
export declare function readConversationDigestInput(workspaceRoot: string, inputPath: string): ConversationDigestInput;
|
|
4
|
+
export declare function provisionalDigestPath(outDir: string, input: Pick<ConversationDigestInput, 'sourcePath' | 'contentHash'>): string;
|
|
5
|
+
export declare function findExistingProvisionalDigest(outDir: string, input: ConversationDigestInput): string | undefined;
|
|
6
|
+
export declare function writeProvisionalDigestFile(outDir: string, input: ConversationDigestInput, markdown: string): string;
|
|
7
|
+
export declare function writeConversationDigestSettings(workspaceRoot: string, digest: ConversationDigestSettings): void;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { PROVISIONAL_DIGEST_DIR } from './constants.js';
|
|
4
|
+
import { atomicWriteTextFile } from '../../utils/files.js';
|
|
5
|
+
import { sha256Hex } from '../../utils/hash.js';
|
|
6
|
+
import { loadProjectSettings, writeProjectSettings } from '../../tools/config.js';
|
|
7
|
+
export function defaultProvisionalDigestDir(workspaceRoot) {
|
|
8
|
+
return path.join(workspaceRoot, PROVISIONAL_DIGEST_DIR);
|
|
9
|
+
}
|
|
10
|
+
export function readConversationDigestInput(workspaceRoot, inputPath) {
|
|
11
|
+
const absoluteInput = path.isAbsolute(inputPath) ? inputPath : path.resolve(workspaceRoot, inputPath);
|
|
12
|
+
const content = fs.readFileSync(absoluteInput, 'utf8');
|
|
13
|
+
return {
|
|
14
|
+
sourcePath: path.relative(workspaceRoot, absoluteInput),
|
|
15
|
+
content,
|
|
16
|
+
contentHash: sha256Hex(content),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export function provisionalDigestPath(outDir, input) {
|
|
20
|
+
const safeName = path
|
|
21
|
+
.basename(input.sourcePath)
|
|
22
|
+
.replace(/\.[^.]+$/, '')
|
|
23
|
+
.replace(/[^a-z0-9._-]+/gi, '-');
|
|
24
|
+
return path.join(outDir, `${input.contentHash.slice(0, 16)}-${safeName}.md`);
|
|
25
|
+
}
|
|
26
|
+
export function findExistingProvisionalDigest(outDir, input) {
|
|
27
|
+
const markdownPath = provisionalDigestPath(outDir, input);
|
|
28
|
+
return fs.existsSync(markdownPath) ? markdownPath : undefined;
|
|
29
|
+
}
|
|
30
|
+
export function writeProvisionalDigestFile(outDir, input, markdown) {
|
|
31
|
+
const markdownPath = provisionalDigestPath(outDir, input);
|
|
32
|
+
atomicWriteTextFile(markdownPath, markdown);
|
|
33
|
+
return markdownPath;
|
|
34
|
+
}
|
|
35
|
+
export function writeConversationDigestSettings(workspaceRoot, digest) {
|
|
36
|
+
const current = loadProjectSettings(workspaceRoot);
|
|
37
|
+
const memory = current.memory ?? {};
|
|
38
|
+
writeProjectSettings(workspaceRoot, {
|
|
39
|
+
memory: {
|
|
40
|
+
...memory,
|
|
41
|
+
enabled: memory.enabled ?? true,
|
|
42
|
+
conversationDigest: digest,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { DigestModelSpec } from './types.js';
|
|
2
|
+
export declare class DigestModelRegistryError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
export declare const DIGEST_MODEL_REGISTRY: Record<string, DigestModelSpec>;
|
|
6
|
+
export declare function getDigestModelSpec(modelId: string): DigestModelSpec;
|