@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.
Files changed (81) hide show
  1. package/dist/__tests__/temp-dir.d.ts +4 -0
  2. package/dist/__tests__/temp-dir.js +20 -0
  3. package/dist/cli/index.d.ts +1 -0
  4. package/dist/cli/index.js +9 -0
  5. package/dist/cli/memory.d.ts +1 -0
  6. package/dist/cli/memory.js +77 -0
  7. package/dist/kit-cli.d.ts +2 -0
  8. package/dist/kit-cli.js +16205 -0
  9. package/dist/kit-server.js +677 -187
  10. package/dist/memory/__tests__/digest-llama-local.test.d.ts +1 -0
  11. package/dist/memory/__tests__/digest-llama-local.test.js +33 -0
  12. package/dist/memory/__tests__/digest-pending.test.d.ts +1 -0
  13. package/dist/memory/__tests__/digest-pending.test.js +164 -0
  14. package/dist/memory/__tests__/digest-processor.test.d.ts +1 -0
  15. package/dist/memory/__tests__/digest-processor.test.js +34 -0
  16. package/dist/memory/__tests__/digest-worker.test.d.ts +1 -0
  17. package/dist/memory/__tests__/digest-worker.test.js +46 -0
  18. package/dist/memory/__tests__/embedder.test.d.ts +1 -0
  19. package/dist/memory/__tests__/embedder.test.js +14 -0
  20. package/dist/memory/__tests__/indexer.test.js +7 -12
  21. package/dist/memory/__tests__/lockfile.test.d.ts +1 -0
  22. package/dist/memory/__tests__/lockfile.test.js +81 -0
  23. package/dist/memory/__tests__/store.test.js +15 -3
  24. package/dist/memory/chunker.js +3 -5
  25. package/dist/memory/constants.d.ts +5 -0
  26. package/dist/memory/constants.js +12 -0
  27. package/dist/memory/digest/constants.d.ts +14 -0
  28. package/dist/memory/digest/constants.js +15 -0
  29. package/dist/memory/digest/files.d.ts +7 -0
  30. package/dist/memory/digest/files.js +45 -0
  31. package/dist/memory/digest/model-registry.d.ts +6 -0
  32. package/dist/memory/digest/model-registry.js +48 -0
  33. package/dist/memory/digest/processor.d.ts +7 -0
  34. package/dist/memory/digest/processor.js +171 -0
  35. package/dist/memory/digest/providers/llama-local.d.ts +7 -0
  36. package/dist/memory/digest/providers/llama-local.js +136 -0
  37. package/dist/memory/digest/types.d.ts +85 -0
  38. package/dist/memory/digest/types.js +1 -0
  39. package/dist/memory/digest/worker.d.ts +3 -0
  40. package/dist/memory/digest/worker.js +51 -0
  41. package/dist/memory/embedder.d.ts +2 -0
  42. package/dist/memory/embedder.js +12 -4
  43. package/dist/memory/index.d.ts +1 -1
  44. package/dist/memory/index.js +1 -1
  45. package/dist/memory/indexer.d.ts +5 -2
  46. package/dist/memory/indexer.js +15 -37
  47. package/dist/memory/store.js +6 -18
  48. package/dist/memory/types.d.ts +2 -1
  49. package/dist/memory/types.js +1 -9
  50. package/dist/tools/__tests__/cli-digest-pending.test.d.ts +1 -0
  51. package/dist/tools/__tests__/cli-digest-pending.test.js +51 -0
  52. package/dist/tools/__tests__/cli-memory.test.d.ts +1 -0
  53. package/dist/tools/__tests__/cli-memory.test.js +22 -0
  54. package/dist/tools/__tests__/digest-init.test.d.ts +1 -0
  55. package/dist/tools/__tests__/digest-init.test.js +32 -0
  56. package/dist/tools/__tests__/memory.test.js +28 -6
  57. package/dist/tools/agent.js +2 -13
  58. package/dist/tools/config.d.ts +4 -4
  59. package/dist/tools/config.js +21 -13
  60. package/dist/tools/core.js +1 -3
  61. package/dist/tools/handoffs.js +1 -2
  62. package/dist/tools/integration.js +2 -8
  63. package/dist/tools/memory.d.ts +1 -1
  64. package/dist/tools/memory.js +29 -3
  65. package/dist/tools/security.js +2 -6
  66. package/dist/utils/args.d.ts +7 -0
  67. package/dist/utils/args.js +31 -0
  68. package/dist/utils/async.d.ts +1 -0
  69. package/dist/utils/async.js +15 -0
  70. package/dist/utils/credentials.d.ts +3 -3
  71. package/dist/utils/credentials.js +5 -7
  72. package/dist/utils/files.d.ts +8 -0
  73. package/dist/utils/files.js +77 -0
  74. package/dist/utils/hash.d.ts +1 -0
  75. package/dist/utils/hash.js +4 -0
  76. package/dist/utils/json.d.ts +2 -0
  77. package/dist/utils/json.js +13 -0
  78. package/dist/utils/parser.js +2 -6
  79. package/dist/utils/paths.d.ts +4 -0
  80. package/dist/utils/paths.js +6 -0
  81. 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
- get dimension() {
16
- return 384;
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
- }], [new Float32Array(384)]);
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({ id: 'del-src-0001', source: 'deleteme.md', content: 'delete source chunk 1' });
58
- const c2 = makeChunk({ id: 'del-src-0002', source: 'deleteme.md', content: 'delete source chunk 2' });
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({ id: 'indexed-src-0001', source: 'indexed-source.md', content: 'indexed' });
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(', ')}`);
@@ -1,6 +1,6 @@
1
- import { createHash } from 'crypto';
1
+ import { sha256Hex } from '../utils/hash.js';
2
2
  export function computeChunkId(content) {
3
- return createHash('sha256').update(content).digest('hex').slice(0, 16);
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,5 @@
1
+ import type { MemoryConfig } from './types.js';
2
+ export declare const DEFAULT_MEMORY_CONFIG: MemoryConfig;
3
+ export declare const LOCK_RETRY_MS = 50;
4
+ export declare const LOCK_TIMEOUT_MS = 500;
5
+ export declare const RRF_K = 60;
@@ -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;