@hanhnd/agent-kit 1.0.32 → 1.0.33

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 (107) hide show
  1. package/dist/cli/commands/cli-digest-pending.test.js +111 -0
  2. package/dist/cli/commands/cli-memory.test.js +34 -0
  3. package/dist/cli/{memory.js → commands/memory.js} +24 -11
  4. package/dist/cli/index.js +2 -2
  5. package/dist/{utils → cli/utils}/args.d.ts +2 -1
  6. package/dist/{kit-cli.js → cli.js} +342 -58
  7. package/dist/{tools/config.d.ts → core/config/index.d.ts} +2 -7
  8. package/dist/{tools/config.js → core/config/index.js} +2 -13
  9. package/dist/core/handoffs/index.d.ts +16 -0
  10. package/dist/{tools/handoffs.js → core/handoffs/index.js} +3 -3
  11. package/dist/{tools/security.d.ts → core/security/index.d.ts} +0 -19
  12. package/dist/{tools/security.js → core/security/index.js} +3 -76
  13. package/dist/entrypoints/cli.js +11 -0
  14. package/dist/entrypoints/server.js +37 -0
  15. package/dist/{tools → mcp}/agent.js +1 -1
  16. package/dist/{tools → mcp}/core.d.ts +1 -1
  17. package/dist/{tools → mcp}/core.js +2 -2
  18. package/dist/{tools → mcp}/integration.js +3 -3
  19. package/dist/mcp/memory.d.ts +15 -0
  20. package/dist/{tools → mcp}/memory.js +15 -13
  21. package/dist/{kit-server.js → server.js} +80 -70
  22. package/dist/services/digest/background.d.ts +11 -0
  23. package/dist/services/digest/background.js +258 -0
  24. package/dist/{memory → services}/digest/constants.d.ts +4 -1
  25. package/dist/{memory → services}/digest/constants.js +6 -2
  26. package/dist/services/digest/digest-background.test.js +206 -0
  27. package/dist/{tools/__tests__ → services/digest}/digest-init.test.js +4 -3
  28. package/dist/{memory/__tests__ → services/digest}/digest-llama-local.test.js +1 -13
  29. package/dist/{memory/__tests__ → services/digest}/digest-pending.test.js +39 -11
  30. package/dist/{memory/__tests__ → services/digest}/digest-processor.test.js +5 -7
  31. package/dist/{memory → services}/digest/files.d.ts +0 -1
  32. package/dist/{memory → services}/digest/files.js +2 -2
  33. package/dist/services/digest/model-registry.d.ts +2 -0
  34. package/dist/{memory → services}/digest/model-registry.js +9 -8
  35. package/dist/services/digest/processor.d.ts +13 -0
  36. package/dist/{memory → services}/digest/processor.js +48 -31
  37. package/dist/services/digest/providers/llama-local.d.ts +2 -0
  38. package/dist/{memory → services}/digest/providers/llama-local.js +3 -4
  39. package/dist/{memory → services}/digest/types.d.ts +34 -2
  40. package/dist/services/digest/types.js +6 -0
  41. package/dist/{memory → services}/digest/worker.d.ts +0 -1
  42. package/dist/{memory → services}/digest/worker.js +2 -2
  43. package/dist/{utils → services/integration}/credentials.js +1 -1
  44. package/dist/{utils/parser.js → services/integration/parser/adf.js} +1 -0
  45. package/dist/{memory → services/memory}/chunker.d.ts +0 -2
  46. package/dist/{memory → services/memory}/chunker.js +3 -3
  47. package/dist/{memory/__tests__ → services/memory}/chunker.test.js +1 -34
  48. package/dist/{memory → services/memory}/constants.d.ts +1 -1
  49. package/dist/{memory → services/memory}/constants.js +2 -1
  50. package/dist/{memory → services/memory}/embedder.d.ts +2 -2
  51. package/dist/{memory → services/memory}/embedder.js +5 -5
  52. package/dist/services/memory/embedder.test.js +13 -0
  53. package/dist/{memory → services/memory}/indexer.d.ts +2 -1
  54. package/dist/{memory → services/memory}/indexer.js +1 -1
  55. package/dist/{memory/__tests__ → services/memory}/indexer.test.js +4 -3
  56. package/dist/{tools/__tests__ → services/memory}/memory.test.js +54 -13
  57. package/dist/{memory/__tests__ → services/memory}/store.test.js +3 -2
  58. package/dist/{memory → services/memory}/types.d.ts +7 -2
  59. package/dist/services/memory/types.js +6 -0
  60. package/dist/utils/files.d.ts +0 -1
  61. package/dist/utils/files.js +1 -1
  62. package/dist/{memory/__tests__/lockfile.test.js → utils/files.test.js} +14 -14
  63. package/dist/utils/index.d.ts +6 -0
  64. package/dist/utils/index.js +6 -0
  65. package/dist/utils/json.d.ts +0 -1
  66. package/dist/utils/json.js +0 -10
  67. package/dist/utils/paths.d.ts +0 -1
  68. package/dist/utils/paths.js +1 -1
  69. package/package.json +6 -6
  70. package/dist/memory/__tests__/digest-worker.test.js +0 -46
  71. package/dist/memory/__tests__/embedder.test.js +0 -14
  72. package/dist/memory/digest/model-registry.d.ts +0 -6
  73. package/dist/memory/digest/processor.d.ts +0 -7
  74. package/dist/memory/digest/providers/llama-local.d.ts +0 -7
  75. package/dist/memory/digest/types.js +0 -1
  76. package/dist/memory/types.js +0 -1
  77. package/dist/tools/__tests__/cli-digest-pending.test.js +0 -51
  78. package/dist/tools/__tests__/cli-memory.test.js +0 -22
  79. package/dist/tools/handoffs.d.ts +0 -22
  80. package/dist/tools/memory.d.ts +0 -16
  81. /package/dist/{tools/__tests__ → cli/commands}/cli-digest-pending.test.d.ts +0 -0
  82. /package/dist/{tools/__tests__ → cli/commands}/cli-memory.test.d.ts +0 -0
  83. /package/dist/cli/{memory.d.ts → commands/memory.d.ts} +0 -0
  84. /package/dist/{utils → cli/utils}/args.js +0 -0
  85. /package/dist/{kit-cli.d.ts → entrypoints/cli.d.ts} +0 -0
  86. /package/dist/{kit-server.d.ts → entrypoints/server.d.ts} +0 -0
  87. /package/dist/{tools → mcp}/agent.d.ts +0 -0
  88. /package/dist/{tools → mcp}/integration.d.ts +0 -0
  89. /package/dist/{memory/__tests__/chunker.test.d.ts → services/digest/digest-background.test.d.ts} +0 -0
  90. /package/dist/{tools/__tests__ → services/digest}/digest-init.test.d.ts +0 -0
  91. /package/dist/{memory/__tests__ → services/digest}/digest-llama-local.test.d.ts +0 -0
  92. /package/dist/{memory/__tests__ → services/digest}/digest-pending.test.d.ts +0 -0
  93. /package/dist/{memory/__tests__ → services/digest}/digest-processor.test.d.ts +0 -0
  94. /package/dist/{utils → services/integration}/credentials.d.ts +0 -0
  95. /package/dist/{utils/parser.d.ts → services/integration/parser/adf.d.ts} +0 -0
  96. /package/dist/{memory/__tests__/digest-worker.test.d.ts → services/memory/chunker.test.d.ts} +0 -0
  97. /package/dist/{memory/__tests__ → services/memory}/embedder.test.d.ts +0 -0
  98. /package/dist/{memory → services/memory}/index.d.ts +0 -0
  99. /package/dist/{memory → services/memory}/index.js +0 -0
  100. /package/dist/{memory/__tests__ → services/memory}/indexer.test.d.ts +0 -0
  101. /package/dist/{tools/__tests__ → services/memory}/memory.test.d.ts +0 -0
  102. /package/dist/{memory → services/memory}/store.d.ts +0 -0
  103. /package/dist/{memory → services/memory}/store.js +0 -0
  104. /package/dist/{memory/__tests__ → services/memory}/store.test.d.ts +0 -0
  105. /package/dist/{memory/__tests__/lockfile.test.d.ts → utils/files.test.d.ts} +0 -0
  106. /package/dist/{__tests__/temp-dir.d.ts → utils/temp-dir.test.d.ts} +0 -0
  107. /package/dist/{__tests__/temp-dir.js → utils/temp-dir.test.js} +0 -0
@@ -1,4 +1,5 @@
1
- export declare const DEFAULT_DIGEST_MODEL_ID = "qwen2.5-1.5b-instruct-q4";
1
+ import { DigestModelId } from './types.js';
2
+ export declare const DEFAULT_DIGEST_MODEL_ID = DigestModelId.DEFAULT;
2
3
  export declare const DEFAULT_DIGEST_MAX_INPUT_CHARS = 3000;
3
4
  export declare const DEFAULT_DIGEST_TIMEOUT_MS = 120000;
4
5
  export declare const DIGEST_TIMEOUT_GRACE_MS = 5000;
@@ -9,6 +10,8 @@ export declare const WIKI_DIR: string;
9
10
  export declare const WIKI_RAW_DIR: string;
10
11
  export declare const PROVISIONAL_DIGEST_DIR: string;
11
12
  export declare const DIGEST_LOCKFILE_REL_PATH: string;
13
+ export declare const DIGEST_WORKER_STATUS_REL_PATH: string;
14
+ export declare const DIGEST_WORKER_LOG_REL_PATH: string;
12
15
  export declare const LLAMA_CONTEXT_SIZE = 4096;
13
16
  export declare const LLAMA_MAX_GENERATED_TOKENS = 512;
14
17
  export declare const LLAMA_TEMPERATURE = 0.1;
@@ -1,5 +1,6 @@
1
1
  import * as path from 'node:path';
2
- export const DEFAULT_DIGEST_MODEL_ID = 'qwen2.5-1.5b-instruct-q4';
2
+ import { DigestModelId } from './types.js';
3
+ export const DEFAULT_DIGEST_MODEL_ID = DigestModelId.DEFAULT;
3
4
  export const DEFAULT_DIGEST_MAX_INPUT_CHARS = 3000;
4
5
  export const DEFAULT_DIGEST_TIMEOUT_MS = 120_000;
5
6
  export const DIGEST_TIMEOUT_GRACE_MS = 5_000;
@@ -9,7 +10,10 @@ export const KIT_DIR = '.agent-kit';
9
10
  export const WIKI_DIR = path.join(KIT_DIR, 'wiki');
10
11
  export const WIKI_RAW_DIR = path.join(WIKI_DIR, 'raw');
11
12
  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
+ const DIGEST_WORKER_DIR = path.join(WIKI_DIR, 'digest');
14
+ export const DIGEST_LOCKFILE_REL_PATH = path.join(DIGEST_WORKER_DIR, 'digest-worker.lock');
15
+ export const DIGEST_WORKER_STATUS_REL_PATH = path.join(DIGEST_WORKER_DIR, 'digest-worker.status.json');
16
+ export const DIGEST_WORKER_LOG_REL_PATH = path.join(DIGEST_WORKER_DIR, 'digest-worker.log');
13
17
  export const LLAMA_CONTEXT_SIZE = 4096;
14
18
  export const LLAMA_MAX_GENERATED_TOKENS = 512;
15
19
  export const LLAMA_TEMPERATURE = 0.1;
@@ -0,0 +1,206 @@
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 '../../utils/temp-dir.test.js';
6
+ import { writeConversationDigestSettings } from './files.js';
7
+ import { launchDigestPendingWorker, runDigestPendingWorker } from './background.js';
8
+ import { DIGEST_WORKER_LOG_REL_PATH, DIGEST_WORKER_STATUS_REL_PATH } from './constants.js';
9
+ import { DigestModelId } from './types.js';
10
+ const tempDirs = createTempDirTracker();
11
+ afterEach(() => {
12
+ tempDirs.cleanup();
13
+ });
14
+ function digestWorkerLogPath(workspace) {
15
+ return path.join(workspace, DIGEST_WORKER_LOG_REL_PATH);
16
+ }
17
+ function digestWorkerStatusPath(workspace) {
18
+ return path.join(workspace, DIGEST_WORKER_STATUS_REL_PATH);
19
+ }
20
+ function readDigestWorkerStatus(workspace) {
21
+ try {
22
+ const p = digestWorkerStatusPath(workspace);
23
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
24
+ }
25
+ catch {
26
+ return undefined;
27
+ }
28
+ }
29
+ function writeDigestWorkerStatus(workspace, status) {
30
+ const p = digestWorkerStatusPath(workspace);
31
+ fs.mkdirSync(path.dirname(p), { recursive: true });
32
+ fs.writeFileSync(p, JSON.stringify(status), 'utf8');
33
+ }
34
+ function writeInitState(workspace) {
35
+ writeConversationDigestSettings(workspace, {
36
+ enabled: true,
37
+ initialized: true,
38
+ modelId: DigestModelId.DEFAULT,
39
+ initializedAt: new Date().toISOString(),
40
+ });
41
+ }
42
+ function writeConvFile(workspace) {
43
+ const rawDir = path.join(workspace, '.agent-kit', 'wiki', 'raw');
44
+ fs.mkdirSync(rawDir, { recursive: true });
45
+ fs.writeFileSync(path.join(rawDir, 'conv_2026-01-01T00-00-00-001Z.md'), '**User:** hello\n', 'utf8');
46
+ }
47
+ function status(overrides = {}) {
48
+ const now = new Date().toISOString();
49
+ return {
50
+ state: 'running',
51
+ pid: process.pid,
52
+ startedAt: now,
53
+ updatedAt: now,
54
+ pendingAtStart: 1,
55
+ processed: 0,
56
+ skipped: 0,
57
+ errors: 0,
58
+ ...overrides,
59
+ };
60
+ }
61
+ describe('digest background helpers', () => {
62
+ test('uses .agent-kit/wiki/digest paths for worker files', () => {
63
+ const workspace = tempDirs.makeTempDir('digest-bg-paths-');
64
+ assert.equal(digestWorkerStatusPath(workspace), path.join(workspace, '.agent-kit', 'wiki', 'digest', 'digest-worker.status.json'));
65
+ assert.equal(digestWorkerLogPath(workspace), path.join(workspace, '.agent-kit', 'wiki', 'digest', 'digest-worker.log'));
66
+ });
67
+ test('writes valid status atomically and tolerates invalid status reads', () => {
68
+ const workspace = tempDirs.makeTempDir('digest-bg-status-');
69
+ const written = status({ state: 'complete', pid: null, processed: 1 });
70
+ writeDigestWorkerStatus(workspace, written);
71
+ assert.deepEqual(readDigestWorkerStatus(workspace), written);
72
+ fs.writeFileSync(digestWorkerStatusPath(workspace), '{bad json', 'utf8');
73
+ assert.equal(readDigestWorkerStatus(workspace), undefined);
74
+ });
75
+ });
76
+ describe('launchDigestPendingWorker', () => {
77
+ test('writes not-initialized and does not spawn before digest init', () => {
78
+ const workspace = tempDirs.makeTempDir('digest-bg-not-init-');
79
+ const result = launchDigestPendingWorker({
80
+ workspaceRoot: workspace,
81
+ args: ['--background'],
82
+ entrypoint: 'cli.js',
83
+ });
84
+ assert.equal(result.ok, true);
85
+ assert.equal(result.spawned, false);
86
+ assert.equal(result.status.state, 'not-initialized');
87
+ assert.equal(readDigestWorkerStatus(workspace)?.state, 'not-initialized');
88
+ });
89
+ test('writes no-pending and does not spawn when initialized with no candidates', () => {
90
+ const workspace = tempDirs.makeTempDir('digest-bg-no-pending-');
91
+ writeInitState(workspace);
92
+ const result = launchDigestPendingWorker({
93
+ workspaceRoot: workspace,
94
+ args: ['--background'],
95
+ entrypoint: 'cli.js',
96
+ });
97
+ assert.equal(result.ok, true);
98
+ assert.equal(result.spawned, false);
99
+ assert.equal(result.status.state, 'no-pending');
100
+ });
101
+ test('writes locked when an existing running worker pid is live', () => {
102
+ const workspace = tempDirs.makeTempDir('digest-bg-locked-status-');
103
+ writeDigestWorkerStatus(workspace, status());
104
+ const result = launchDigestPendingWorker({
105
+ workspaceRoot: workspace,
106
+ args: ['--background'],
107
+ entrypoint: 'cli.js',
108
+ });
109
+ assert.equal(result.spawned, false);
110
+ assert.equal(result.status.state, 'locked');
111
+ });
112
+ test('marks stale running status before continuing to no-pending preflight', () => {
113
+ const workspace = tempDirs.makeTempDir('digest-bg-stale-status-');
114
+ writeInitState(workspace);
115
+ writeDigestWorkerStatus(workspace, status({ pid: 999999999 }));
116
+ const result = launchDigestPendingWorker({
117
+ workspaceRoot: workspace,
118
+ args: ['--background'],
119
+ entrypoint: 'cli.js',
120
+ });
121
+ assert.equal(result.spawned, false);
122
+ assert.equal(result.status.state, 'no-pending');
123
+ });
124
+ test('tolerates corrupt stale lock files and continues preflight', () => {
125
+ const workspace = tempDirs.makeTempDir('digest-bg-stale-lock-');
126
+ writeInitState(workspace);
127
+ const lockDir = path.join(workspace, '.agent-kit', 'wiki', 'digest');
128
+ fs.mkdirSync(lockDir, { recursive: true });
129
+ fs.writeFileSync(path.join(lockDir, 'digest-worker.lock'), 'not-json', 'utf8');
130
+ const result = launchDigestPendingWorker({
131
+ workspaceRoot: workspace,
132
+ args: ['--background'],
133
+ entrypoint: 'cli.js',
134
+ });
135
+ assert.equal(result.spawned, false);
136
+ assert.equal(result.status.state, 'no-pending');
137
+ });
138
+ test('writes failed status when pending work exists but entrypoint is unavailable', () => {
139
+ const workspace = tempDirs.makeTempDir('digest-bg-spawn-failed-');
140
+ writeInitState(workspace);
141
+ writeConvFile(workspace);
142
+ const result = launchDigestPendingWorker({
143
+ workspaceRoot: workspace,
144
+ args: ['--background'],
145
+ entrypoint: undefined,
146
+ });
147
+ assert.equal(result.ok, false);
148
+ assert.equal(result.spawned, false);
149
+ assert.equal(result.status.state, 'failed');
150
+ assert.equal(result.status.pendingAtStart, 1);
151
+ });
152
+ });
153
+ describe('runDigestPendingWorker', () => {
154
+ test('writes final no-pending status for initialized workspace without candidates', async () => {
155
+ const workspace = tempDirs.makeTempDir('digest-bg-worker-no-pending-');
156
+ writeInitState(workspace);
157
+ const result = await runDigestPendingWorker({ workspaceRoot: workspace, pendingAtStart: 0 });
158
+ assert.equal(result.state, 'no-pending');
159
+ assert.equal(readDigestWorkerStatus(workspace)?.state, 'no-pending');
160
+ assert.match(fs.readFileSync(digestWorkerLogPath(workspace), 'utf8'), /worker finished/);
161
+ });
162
+ test('writes failed final status for run-level processor failure', async () => {
163
+ const workspace = tempDirs.makeTempDir('digest-bg-worker-failed-');
164
+ writeInitState(workspace);
165
+ const wikiDir = path.join(workspace, '.agent-kit', 'wiki');
166
+ fs.mkdirSync(wikiDir, { recursive: true });
167
+ fs.writeFileSync(path.join(wikiDir, 'raw'), 'not-a-directory', 'utf8');
168
+ const result = await runDigestPendingWorker({ workspaceRoot: workspace, pendingAtStart: 1 });
169
+ assert.equal(result.state, 'failed');
170
+ assert.equal(result.errors, 1);
171
+ assert.ok(result.lastError);
172
+ });
173
+ test('updates status file in real-time as conversations are digested', async () => {
174
+ const workspace = tempDirs.makeTempDir('digest-bg-worker-realtime-');
175
+ writeInitState(workspace);
176
+ const rawDir = path.join(workspace, '.agent-kit', 'wiki', 'raw');
177
+ fs.mkdirSync(rawDir, { recursive: true });
178
+ fs.writeFileSync(path.join(rawDir, 'conv_1.md'), '**User:** one\n', 'utf8');
179
+ fs.writeFileSync(path.join(rawDir, 'conv_2.md'), '**User:** two\n', 'utf8');
180
+ const statusesSeen = [];
181
+ const mockDigestFn = async () => {
182
+ statusesSeen.push(readDigestWorkerStatus(workspace));
183
+ return {
184
+ markdown: 'out.md',
185
+ status: 'provisional',
186
+ contentHash: 'hash',
187
+ skipped: false,
188
+ indexed: false,
189
+ };
190
+ };
191
+ const result = await runDigestPendingWorker({
192
+ workspaceRoot: workspace,
193
+ pendingAtStart: 2,
194
+ digestFn: mockDigestFn,
195
+ });
196
+ assert.equal(statusesSeen.length, 2);
197
+ assert.ok(statusesSeen[0]);
198
+ assert.equal(statusesSeen[0].state, 'running');
199
+ assert.equal(statusesSeen[0].processed, 0);
200
+ assert.ok(statusesSeen[1]);
201
+ assert.equal(statusesSeen[1].state, 'running');
202
+ assert.equal(statusesSeen[1].processed, 1);
203
+ assert.equal(result.state, 'complete');
204
+ assert.equal(result.processed, 2);
205
+ });
206
+ });
@@ -2,8 +2,9 @@ import * as assert from 'node:assert/strict';
2
2
  import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
4
  import { afterEach, describe, test } from 'node:test';
5
- import { createTempDirTracker } from '../../__tests__/temp-dir.js';
6
- import { initializeConversationDigestModel } from '../../memory/digest/processor.js';
5
+ import { createTempDirTracker } from '../../utils/temp-dir.test.js';
6
+ import { initializeConversationDigestModel } from './processor.js';
7
+ import { DigestModelId } from './types.js';
7
8
  const tempDirs = createTempDirTracker();
8
9
  afterEach(() => {
9
10
  tempDirs.cleanup();
@@ -23,7 +24,7 @@ describe('initializeConversationDigestModel', () => {
23
24
  const workspace = tempDirs.makeTempDir('digest-init-');
24
25
  const result = await initializeConversationDigestModel({
25
26
  workspaceRoot: workspace,
26
- modelId: 'qwen2.5-1.5b-instruct-q4',
27
+ modelId: DigestModelId.DEFAULT,
27
28
  allowDownload: false,
28
29
  });
29
30
  assert.equal(result.initialized, false);
@@ -1,24 +1,12 @@
1
1
  import * as assert from 'node:assert/strict';
2
2
  import { describe, test } from 'node:test';
3
- import { createLlamaLocalDigestProvider, sanitizeConversationDigestMarkdown, trimConversationExport, } from '../digest/providers/llama-local.js';
3
+ import { createLlamaLocalDigestProvider } from '../digest/providers/llama-local.js';
4
4
  const INPUT = {
5
5
  sourcePath: '.agent-kit/wiki/archive/conversations/conv_2026-05-19T00-45-17-697Z.md',
6
6
  contentHash: 'tiny',
7
7
  content: '**User:** use local digest generation for archived conversations',
8
8
  };
9
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
10
  test('runs an opt-in smoke test against a real model', { skip: !process.env.AGENT_KIT_DIGEST_MODEL_ID }, async () => {
23
11
  const modelId = process.env.AGENT_KIT_DIGEST_MODEL_ID;
24
12
  const provider = await createLlamaLocalDigestProvider(modelId);
@@ -2,14 +2,15 @@ import * as assert from 'node:assert/strict';
2
2
  import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
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';
5
+ import { createTempDirTracker } from '../../utils/temp-dir.test.js';
6
+ import { defaultProvisionalDigestDir, readConversationDigestInput, writeConversationDigestSettings, writeProvisionalDigestFile, } from './files.js';
7
+ import { DigestModelId } from './types.js';
8
+ import { digestPendingConversations, summarizePendingConversations } from './processor.js';
8
9
  const tempDirs = createTempDirTracker();
9
10
  afterEach(() => {
10
11
  tempDirs.cleanup();
11
12
  });
12
- function writeInitState(workspace, modelId = 'qwen2.5-1.5b-instruct-q4') {
13
+ function writeInitState(workspace, modelId = DigestModelId.DEFAULT) {
13
14
  writeConversationDigestSettings(workspace, {
14
15
  enabled: true,
15
16
  initialized: true,
@@ -47,7 +48,7 @@ describe('digestPendingConversations', () => {
47
48
  test('C14: init state, live lockfile → noop locked', async () => {
48
49
  const workspace = tempDirs.makeTempDir('pending-c14-');
49
50
  writeInitState(workspace);
50
- const lockDir = path.join(workspace, '.agent-kit');
51
+ const lockDir = path.join(workspace, '.agent-kit', 'wiki', 'digest');
51
52
  fs.mkdirSync(lockDir, { recursive: true });
52
53
  fs.writeFileSync(path.join(lockDir, 'digest-worker.lock'), JSON.stringify({ pid: process.pid }));
53
54
  const result = await digestPendingConversations({ workspaceRoot: workspace });
@@ -65,7 +66,7 @@ describe('digestPendingConversations', () => {
65
66
  assert.equal(result.action, 'noop');
66
67
  if (result.action === 'noop')
67
68
  assert.equal(result.reason, 'no-pending');
68
- assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'digest-worker.lock')), false);
69
+ assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'wiki', 'digest', 'digest-worker.lock')), false);
69
70
  });
70
71
  test('C19: wiki/raw dir does not exist → noop no-pending', async () => {
71
72
  const workspace = tempDirs.makeTempDir('pending-c19-');
@@ -89,7 +90,7 @@ describe('digestPendingConversations', () => {
89
90
  assert.equal(result.count, 1);
90
91
  assert.equal(result.errors, 0);
91
92
  }
92
- assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'digest-worker.lock')), false);
93
+ assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'wiki', 'digest', 'digest-worker.lock')), false);
93
94
  });
94
95
  test('C17: digestFn throws for one file → errors:1, lock released', async () => {
95
96
  const workspace = tempDirs.makeTempDir('pending-c17-');
@@ -108,19 +109,19 @@ describe('digestPendingConversations', () => {
108
109
  assert.equal(result.errors, 1);
109
110
  assert.equal(result.count, 0);
110
111
  }
111
- assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'digest-worker.lock')), false);
112
+ assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'wiki', 'digest', 'digest-worker.lock')), false);
112
113
  });
113
114
  test('C18: stale lockfile (dead PID) → reclaims lock and returns (no-pending since no conv files)', async () => {
114
115
  const workspace = tempDirs.makeTempDir('pending-c18-');
115
116
  writeInitState(workspace);
116
- const lockDir = path.join(workspace, '.agent-kit');
117
+ const lockDir = path.join(workspace, '.agent-kit', 'wiki', 'digest');
117
118
  fs.mkdirSync(lockDir, { recursive: true });
118
119
  fs.writeFileSync(path.join(lockDir, 'digest-worker.lock'), JSON.stringify({ pid: 999999999 }));
119
120
  const result = await digestPendingConversations({ workspaceRoot: workspace });
120
121
  assert.ok(result.ok);
121
122
  if (result.action === 'noop')
122
123
  assert.notEqual(result.reason, 'locked');
123
- assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'digest-worker.lock')), false);
124
+ assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'wiki', 'digest', 'digest-worker.lock')), false);
124
125
  });
125
126
  test('C20: 3 undigested conv files → digestFn called 3 times, count+skipped+errors=3', async () => {
126
127
  const workspace = tempDirs.makeTempDir('pending-c20-');
@@ -159,6 +160,33 @@ describe('digestPendingConversations', () => {
159
160
  const result = await digestPendingConversations({ workspaceRoot: workspace });
160
161
  assert.equal(result.ok, false);
161
162
  assert.equal(result.action, 'error');
162
- assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'digest-worker.lock')), false);
163
+ assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'wiki', 'digest', 'digest-worker.lock')), false);
164
+ });
165
+ });
166
+ describe('summarizePendingConversations', () => {
167
+ test('returns not-initialized without raw discovery side effects', () => {
168
+ const workspace = tempDirs.makeTempDir('pending-summary-init-');
169
+ const result = summarizePendingConversations(workspace);
170
+ assert.equal(result.initialized, false);
171
+ assert.equal(result.pending, 0);
172
+ assert.equal(result.reason, 'not-initialized');
173
+ });
174
+ test('returns no-pending when initialized workspace has no raw conversations', () => {
175
+ const workspace = tempDirs.makeTempDir('pending-summary-empty-');
176
+ writeInitState(workspace);
177
+ const result = summarizePendingConversations(workspace);
178
+ assert.equal(result.initialized, true);
179
+ assert.equal(result.pending, 0);
180
+ assert.equal(result.reason, 'no-pending');
181
+ });
182
+ test('counts only conversations without existing provisional digests', () => {
183
+ const workspace = tempDirs.makeTempDir('pending-summary-count-');
184
+ writeInitState(workspace);
185
+ const first = writeConvFile(workspace, 'conv_2026-01-01T00-00-00-001Z.md', '**User:** a\n');
186
+ writeConvFile(workspace, 'conv_2026-01-01T00-00-00-002Z.md', '**User:** b\n');
187
+ writeProvisionalDigestFile(defaultProvisionalDigestDir(workspace), readConversationDigestInput(workspace, first), '# Existing\n');
188
+ const result = summarizePendingConversations(workspace);
189
+ assert.equal(result.initialized, true);
190
+ assert.equal(result.pending, 1);
163
191
  });
164
192
  });
@@ -2,10 +2,10 @@ import * as assert from 'node:assert/strict';
2
2
  import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
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';
5
+ import { createTempDirTracker } from '../../utils/temp-dir.test.js';
6
+ import { DEFAULT_DIGEST_MODEL_ID } from './constants.js';
7
+ import { defaultProvisionalDigestDir, writeProvisionalDigestFile, readConversationDigestInput } from './files.js';
8
+ import { digestConversationFile } from './processor.js';
9
9
  const tempDirs = createTempDirTracker();
10
10
  afterEach(() => {
11
11
  tempDirs.cleanup();
@@ -18,9 +18,7 @@ describe('digestConversationFile', () => {
18
18
  fs.writeFileSync(inputPath, '**User:** remember this decision', 'utf8');
19
19
  const input = readConversationDigestInput(workspace, inputPath);
20
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');
21
+ const markdownPath = writeProvisionalDigestFile(outDir, input, '# Conversation Digest: conv\n');
24
22
  const result = await digestConversationFile({
25
23
  workspaceRoot: workspace,
26
24
  inputPath,
@@ -1,7 +1,6 @@
1
1
  import type { ConversationDigestInput, ConversationDigestSettings } from './types.js';
2
2
  export declare function defaultProvisionalDigestDir(workspaceRoot: string): string;
3
3
  export declare function readConversationDigestInput(workspaceRoot: string, inputPath: string): ConversationDigestInput;
4
- export declare function provisionalDigestPath(outDir: string, input: Pick<ConversationDigestInput, 'sourcePath' | 'contentHash'>): string;
5
4
  export declare function findExistingProvisionalDigest(outDir: string, input: ConversationDigestInput): string | undefined;
6
5
  export declare function writeProvisionalDigestFile(outDir: string, input: ConversationDigestInput, markdown: string): string;
7
6
  export declare function writeConversationDigestSettings(workspaceRoot: string, digest: ConversationDigestSettings): void;
@@ -3,7 +3,7 @@ import * as path from 'node:path';
3
3
  import { PROVISIONAL_DIGEST_DIR } from './constants.js';
4
4
  import { atomicWriteTextFile } from '../../utils/files.js';
5
5
  import { sha256Hex } from '../../utils/hash.js';
6
- import { loadProjectSettings, writeProjectSettings } from '../../tools/config.js';
6
+ import { loadProjectSettings, writeProjectSettings } from '../../core/config/index.js';
7
7
  export function defaultProvisionalDigestDir(workspaceRoot) {
8
8
  return path.join(workspaceRoot, PROVISIONAL_DIGEST_DIR);
9
9
  }
@@ -16,7 +16,7 @@ export function readConversationDigestInput(workspaceRoot, inputPath) {
16
16
  contentHash: sha256Hex(content),
17
17
  };
18
18
  }
19
- export function provisionalDigestPath(outDir, input) {
19
+ function provisionalDigestPath(outDir, input) {
20
20
  const safeName = path
21
21
  .basename(input.sourcePath)
22
22
  .replace(/\.[^.]+$/, '')
@@ -0,0 +1,2 @@
1
+ import { type DigestModelSpec } from './types.js';
2
+ export declare function getDigestModelSpec(modelId: string): DigestModelSpec;
@@ -1,28 +1,29 @@
1
- export class DigestModelRegistryError extends Error {
1
+ import { DigestModelId } from './types.js';
2
+ class DigestModelRegistryError extends Error {
2
3
  constructor(message) {
3
4
  super(message);
4
5
  this.name = 'DigestModelRegistryError';
5
6
  }
6
7
  }
7
- export const DIGEST_MODEL_REGISTRY = {
8
- 'qwen2.5-0.5b-instruct-q4': {
9
- id: 'qwen2.5-0.5b-instruct-q4',
8
+ const DIGEST_MODEL_REGISTRY = {
9
+ [DigestModelId.TINY]: {
10
+ id: DigestModelId.TINY,
10
11
  ggufUri: 'hf:bartowski/Qwen2.5-0.5B-Instruct-GGUF/Qwen2.5-0.5B-Instruct-Q4_K_M.gguf',
11
12
  approxSizeBytes: 400_000_000,
12
13
  license: 'Apache-2.0',
13
14
  sourceUrl: 'https://huggingface.co/bartowski/Qwen2.5-0.5B-Instruct-GGUF',
14
15
  enabled: true,
15
16
  },
16
- 'qwen2.5-1.5b-instruct-q4': {
17
- id: 'qwen2.5-1.5b-instruct-q4',
17
+ [DigestModelId.DEFAULT]: {
18
+ id: DigestModelId.DEFAULT,
18
19
  ggufUri: 'hf:bartowski/Qwen2.5-1.5B-Instruct-GGUF/Qwen2.5-1.5B-Instruct-Q4_K_M.gguf',
19
20
  approxSizeBytes: 1_000_000_000,
20
21
  license: 'Apache-2.0',
21
22
  sourceUrl: 'https://huggingface.co/bartowski/Qwen2.5-1.5B-Instruct-GGUF',
22
23
  enabled: true,
23
24
  },
24
- 'qwen2.5-3b-instruct-q4': {
25
- id: 'qwen2.5-3b-instruct-q4',
25
+ [DigestModelId.LARGE]: {
26
+ id: DigestModelId.LARGE,
26
27
  ggufUri: 'hf:bartowski/Qwen2.5-3B-Instruct-GGUF/Qwen2.5-3B-Instruct-Q4_K_M.gguf',
27
28
  approxSizeBytes: 1_900_000_000,
28
29
  license: 'Apache-2.0',
@@ -0,0 +1,13 @@
1
+ import type { DigestFileOptions, DigestPendingCandidateSummary, DigestPendingResult, ProvisionalDigestResult, ConversationDigestInitResult, InitializeConversationDigestInput } from './types.js';
2
+ export declare function digestConversationFile(options: DigestFileOptions): Promise<ProvisionalDigestResult>;
3
+ export declare function summarizePendingConversations(workspaceRoot: string): DigestPendingCandidateSummary;
4
+ export declare function digestPendingConversations({ workspaceRoot, digestFn, onProgress, }: {
5
+ workspaceRoot: string;
6
+ digestFn?: (opts: DigestFileOptions) => Promise<ProvisionalDigestResult>;
7
+ onProgress?: (progress: {
8
+ processed: number;
9
+ skipped: number;
10
+ errors: number;
11
+ }) => void | Promise<void>;
12
+ }): Promise<DigestPendingResult>;
13
+ export declare function initializeConversationDigestModel(input: InitializeConversationDigestInput): Promise<ConversationDigestInitResult>;
@@ -4,13 +4,42 @@ import { DEFAULT_DIGEST_TIMEOUT_MS, DEFAULT_DIGEST_MAX_INPUT_CHARS, DIGEST_LOCKF
4
4
  import { defaultProvisionalDigestDir, readConversationDigestInput, findExistingProvisionalDigest, writeProvisionalDigestFile, writeConversationDigestSettings, } from './files.js';
5
5
  import { getDigestModelSpec } from './model-registry.js';
6
6
  import { createLlamaLocalDigestProvider } from './providers/llama-local.js';
7
- import { loadProjectSettings, resolveConversationDigestConfig, resolveMemoryConfig } from '../../tools/config.js';
8
- import { MemoryStore } from '../store.js';
9
- import { MemoryIndexer } from '../indexer.js';
7
+ import { loadProjectSettings, resolveConversationDigestConfig, resolveMemoryConfig } from '../../core/config/index.js';
8
+ import { MemoryStore } from '../memory/store.js';
9
+ import { MemoryIndexer } from '../memory/indexer.js';
10
10
  import { releaseLock, tryAcquireProcessLock } from '../../utils/files.js';
11
11
  function digestLockPath(workspaceRoot) {
12
12
  return path.join(workspaceRoot, DIGEST_LOCKFILE_REL_PATH);
13
13
  }
14
+ function discoverPendingConversationFiles(workspaceRoot) {
15
+ const rawDir = path.join(workspaceRoot, WIKI_RAW_DIR);
16
+ let files;
17
+ try {
18
+ files = fs
19
+ .readdirSync(rawDir)
20
+ .filter((f) => f.startsWith('conv_') && f.endsWith('.md'))
21
+ .map((f) => path.join(rawDir, f))
22
+ .sort((a, b) => fs.statSync(a).mtimeMs - fs.statSync(b).mtimeMs);
23
+ }
24
+ catch (err) {
25
+ if (err.code === 'ENOENT') {
26
+ files = [];
27
+ }
28
+ else {
29
+ throw err;
30
+ }
31
+ }
32
+ const outDir = defaultProvisionalDigestDir(workspaceRoot);
33
+ return files.filter((f) => {
34
+ try {
35
+ const input = readConversationDigestInput(workspaceRoot, f);
36
+ return !findExistingProvisionalDigest(outDir, input);
37
+ }
38
+ catch {
39
+ return false;
40
+ }
41
+ });
42
+ }
14
43
  async function indexProvisionalDigestFile(workspaceRoot, markdownPath) {
15
44
  try {
16
45
  const settings = loadProjectSettings(workspaceRoot);
@@ -63,7 +92,18 @@ export async function digestConversationFile(options) {
63
92
  await provider.dispose?.();
64
93
  }
65
94
  }
66
- export async function digestPendingConversations({ workspaceRoot, digestFn = digestConversationFile, }) {
95
+ export function summarizePendingConversations(workspaceRoot) {
96
+ const digestConfig = resolveConversationDigestConfig(loadProjectSettings(workspaceRoot));
97
+ if (!digestConfig || digestConfig.enabled === false) {
98
+ return { initialized: false, pending: 0, reason: 'not-initialized' };
99
+ }
100
+ const candidates = discoverPendingConversationFiles(workspaceRoot);
101
+ if (candidates.length === 0) {
102
+ return { initialized: true, pending: 0, reason: 'no-pending' };
103
+ }
104
+ return { initialized: true, pending: candidates.length };
105
+ }
106
+ export async function digestPendingConversations({ workspaceRoot, digestFn = digestConversationFile, onProgress, }) {
67
107
  const digestConfig = resolveConversationDigestConfig(loadProjectSettings(workspaceRoot));
68
108
  if (!digestConfig || digestConfig.enabled === false) {
69
109
  return { ok: true, initialized: false, action: 'noop', reason: 'not-initialized' };
@@ -73,33 +113,7 @@ export async function digestPendingConversations({ workspaceRoot, digestFn = dig
73
113
  return { ok: true, initialized: true, action: 'noop', reason: 'locked' };
74
114
  }
75
115
  try {
76
- const rawDir = path.join(workspaceRoot, WIKI_RAW_DIR);
77
- let files;
78
- try {
79
- files = fs
80
- .readdirSync(rawDir)
81
- .filter((f) => f.startsWith('conv_') && f.endsWith('.md'))
82
- .map((f) => path.join(rawDir, f))
83
- .sort((a, b) => fs.statSync(a).mtimeMs - fs.statSync(b).mtimeMs);
84
- }
85
- catch (err) {
86
- if (err.code === 'ENOENT') {
87
- files = [];
88
- }
89
- else {
90
- throw err;
91
- }
92
- }
93
- const outDir = defaultProvisionalDigestDir(workspaceRoot);
94
- const candidates = files.filter((f) => {
95
- try {
96
- const input = readConversationDigestInput(workspaceRoot, f);
97
- return !findExistingProvisionalDigest(outDir, input);
98
- }
99
- catch {
100
- return false;
101
- }
102
- });
116
+ const candidates = discoverPendingConversationFiles(workspaceRoot);
103
117
  if (candidates.length === 0) {
104
118
  return { ok: true, initialized: true, action: 'noop', reason: 'no-pending' };
105
119
  }
@@ -123,6 +137,9 @@ export async function digestPendingConversations({ workspaceRoot, digestFn = dig
123
137
  catch {
124
138
  errors++;
125
139
  }
140
+ if (onProgress) {
141
+ await onProgress({ processed: count, skipped, errors });
142
+ }
126
143
  }
127
144
  return { ok: true, initialized: true, action: 'digested', count, skipped, errors };
128
145
  }
@@ -0,0 +1,2 @@
1
+ import type { ConversationDigestProvider } from '../types.js';
2
+ export declare function createLlamaLocalDigestProvider(modelId: string): Promise<ConversationDigestProvider>;