@hanhnd/agent-kit 1.0.31 → 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.
- package/dist/cli/commands/cli-digest-pending.test.js +111 -0
- package/dist/cli/commands/cli-memory.test.js +34 -0
- package/dist/cli/commands/memory.d.ts +1 -0
- package/dist/cli/commands/memory.js +90 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +9 -0
- package/dist/cli/utils/args.d.ts +8 -0
- package/dist/cli/utils/args.js +31 -0
- package/dist/cli.js +16489 -0
- package/dist/{tools/config.d.ts → core/config/index.d.ts} +4 -9
- package/dist/{tools/config.js → core/config/index.js} +20 -23
- package/dist/core/handoffs/index.d.ts +16 -0
- package/dist/{tools/handoffs.js → core/handoffs/index.js} +4 -5
- package/dist/{tools/security.d.ts → core/security/index.d.ts} +0 -19
- package/dist/{tools/security.js → core/security/index.js} +3 -80
- package/dist/entrypoints/cli.d.ts +2 -0
- package/dist/entrypoints/cli.js +11 -0
- package/dist/entrypoints/server.js +37 -0
- package/dist/{tools → mcp}/agent.js +3 -14
- package/dist/{tools → mcp}/core.d.ts +1 -1
- package/dist/{tools → mcp}/core.js +3 -5
- package/dist/{tools → mcp}/integration.js +5 -11
- package/dist/mcp/memory.d.ts +15 -0
- package/dist/{tools → mcp}/memory.js +40 -12
- package/dist/{kit-server.js → server.js} +720 -224
- package/dist/services/digest/background.d.ts +11 -0
- package/dist/services/digest/background.js +258 -0
- package/dist/services/digest/constants.d.ts +17 -0
- package/dist/services/digest/constants.js +19 -0
- package/dist/services/digest/digest-background.test.js +206 -0
- package/dist/services/digest/digest-init.test.js +33 -0
- package/dist/services/digest/digest-llama-local.test.js +21 -0
- package/dist/services/digest/digest-pending.test.d.ts +1 -0
- package/dist/services/digest/digest-pending.test.js +192 -0
- package/dist/services/digest/digest-processor.test.d.ts +1 -0
- package/dist/services/digest/digest-processor.test.js +32 -0
- package/dist/services/digest/files.d.ts +6 -0
- package/dist/services/digest/files.js +45 -0
- package/dist/services/digest/model-registry.d.ts +2 -0
- package/dist/services/digest/model-registry.js +49 -0
- package/dist/services/digest/processor.d.ts +13 -0
- package/dist/services/digest/processor.js +188 -0
- package/dist/services/digest/providers/llama-local.d.ts +2 -0
- package/dist/services/digest/providers/llama-local.js +135 -0
- package/dist/services/digest/types.d.ts +117 -0
- package/dist/services/digest/types.js +6 -0
- package/dist/services/digest/worker.d.ts +2 -0
- package/dist/services/digest/worker.js +51 -0
- package/dist/{utils → services/integration}/credentials.js +1 -1
- package/dist/{utils/parser.js → services/integration/parser/adf.js} +3 -6
- package/dist/{memory → services/memory}/chunker.d.ts +0 -2
- package/dist/{memory → services/memory}/chunker.js +5 -7
- package/dist/services/memory/chunker.test.d.ts +1 -0
- package/dist/{memory/__tests__ → services/memory}/chunker.test.js +1 -34
- package/dist/services/memory/constants.d.ts +5 -0
- package/dist/services/memory/constants.js +13 -0
- package/dist/{memory → services/memory}/embedder.d.ts +3 -2
- package/dist/{memory → services/memory}/embedder.js +15 -6
- package/dist/services/memory/embedder.test.d.ts +1 -0
- package/dist/services/memory/embedder.test.js +13 -0
- package/dist/{memory → services/memory}/index.d.ts +1 -1
- package/dist/{memory → services/memory}/index.js +1 -1
- package/dist/{memory → services/memory}/indexer.d.ts +6 -2
- package/dist/{memory → services/memory}/indexer.js +15 -37
- package/dist/services/memory/indexer.test.d.ts +1 -0
- package/dist/{memory/__tests__ → services/memory}/indexer.test.js +11 -15
- package/dist/services/memory/memory.test.d.ts +1 -0
- package/dist/{tools/__tests__ → services/memory}/memory.test.js +82 -19
- package/dist/{memory → services/memory}/store.js +6 -18
- package/dist/services/memory/store.test.d.ts +1 -0
- package/dist/{memory/__tests__ → services/memory}/store.test.js +18 -5
- package/dist/{memory → services/memory}/types.d.ts +8 -2
- package/dist/services/memory/types.js +6 -0
- package/dist/utils/async.d.ts +1 -0
- package/dist/utils/async.js +15 -0
- package/dist/utils/files.d.ts +7 -0
- package/dist/utils/files.js +77 -0
- package/dist/utils/files.test.d.ts +1 -0
- package/dist/utils/files.test.js +81 -0
- package/dist/utils/hash.d.ts +1 -0
- package/dist/utils/hash.js +4 -0
- package/dist/utils/index.d.ts +6 -0
- package/dist/utils/index.js +6 -0
- package/dist/utils/json.d.ts +1 -0
- package/dist/utils/json.js +3 -0
- package/dist/utils/paths.d.ts +0 -1
- package/dist/utils/paths.js +1 -1
- package/dist/utils/temp-dir.test.d.ts +4 -0
- package/dist/utils/temp-dir.test.js +20 -0
- package/package.json +8 -6
- package/dist/memory/__tests__/embedder.test.js +0 -14
- package/dist/memory/types.js +0 -9
- package/dist/tools/handoffs.d.ts +0 -22
- package/dist/tools/memory.d.ts +0 -16
- /package/dist/{memory/__tests__/chunker.test.d.ts → cli/commands/cli-digest-pending.test.d.ts} +0 -0
- /package/dist/{memory/__tests__/embedder.test.d.ts → cli/commands/cli-memory.test.d.ts} +0 -0
- /package/dist/{kit-server.d.ts → entrypoints/server.d.ts} +0 -0
- /package/dist/{tools → mcp}/agent.d.ts +0 -0
- /package/dist/{tools → mcp}/integration.d.ts +0 -0
- /package/dist/{memory/__tests__/indexer.test.d.ts → services/digest/digest-background.test.d.ts} +0 -0
- /package/dist/{memory/__tests__/store.test.d.ts → services/digest/digest-init.test.d.ts} +0 -0
- /package/dist/{tools/__tests__/memory.test.d.ts → services/digest/digest-llama-local.test.d.ts} +0 -0
- /package/dist/{utils → services/integration}/credentials.d.ts +0 -0
- /package/dist/{utils/parser.d.ts → services/integration/parser/adf.d.ts} +0 -0
- /package/dist/{memory → services/memory}/store.d.ts +0 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DigestPendingLauncherResult, DigestWorkerStatus, DigestFileOptions, ProvisionalDigestResult } from './types.js';
|
|
2
|
+
export declare function runDigestPendingWorker(input: {
|
|
3
|
+
workspaceRoot: string;
|
|
4
|
+
pendingAtStart?: number;
|
|
5
|
+
digestFn?: (opts: DigestFileOptions) => Promise<ProvisionalDigestResult>;
|
|
6
|
+
}): Promise<DigestWorkerStatus>;
|
|
7
|
+
export declare function launchDigestPendingWorker(input: {
|
|
8
|
+
workspaceRoot: string;
|
|
9
|
+
args: string[];
|
|
10
|
+
entrypoint: string | undefined;
|
|
11
|
+
}): DigestPendingLauncherResult;
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { DIGEST_LOCKFILE_REL_PATH, DIGEST_WORKER_FLAG, DIGEST_WORKER_LOG_REL_PATH, DIGEST_WORKER_STATUS_REL_PATH, } from './constants.js';
|
|
5
|
+
import { digestPendingConversations, summarizePendingConversations } from './processor.js';
|
|
6
|
+
import { atomicWriteJsonFile } from '../../utils/files.js';
|
|
7
|
+
import { isRecord } from '../../utils/json.js';
|
|
8
|
+
function digestWorkerStatusPath(workspaceRoot) {
|
|
9
|
+
return path.join(workspaceRoot, DIGEST_WORKER_STATUS_REL_PATH);
|
|
10
|
+
}
|
|
11
|
+
function digestWorkerLogPath(workspaceRoot) {
|
|
12
|
+
return path.join(workspaceRoot, DIGEST_WORKER_LOG_REL_PATH);
|
|
13
|
+
}
|
|
14
|
+
function digestWorkerLockPath(workspaceRoot) {
|
|
15
|
+
return path.join(workspaceRoot, DIGEST_LOCKFILE_REL_PATH);
|
|
16
|
+
}
|
|
17
|
+
function isoNow() {
|
|
18
|
+
return new Date().toISOString();
|
|
19
|
+
}
|
|
20
|
+
function errorMessage(err) {
|
|
21
|
+
return err instanceof Error ? err.message : String(err);
|
|
22
|
+
}
|
|
23
|
+
function emptyStatus(state, pendingAtStart, startedAt = isoNow()) {
|
|
24
|
+
return {
|
|
25
|
+
state,
|
|
26
|
+
pid: null,
|
|
27
|
+
startedAt,
|
|
28
|
+
updatedAt: startedAt,
|
|
29
|
+
pendingAtStart,
|
|
30
|
+
processed: 0,
|
|
31
|
+
skipped: 0,
|
|
32
|
+
errors: 0,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const WORKER_STATES = new Set([
|
|
36
|
+
'running',
|
|
37
|
+
'complete',
|
|
38
|
+
'failed',
|
|
39
|
+
'not-initialized',
|
|
40
|
+
'no-pending',
|
|
41
|
+
'locked',
|
|
42
|
+
'stale',
|
|
43
|
+
]);
|
|
44
|
+
function isDigestWorkerStatus(value) {
|
|
45
|
+
if (!isRecord(value))
|
|
46
|
+
return false;
|
|
47
|
+
return (typeof value.state === 'string' &&
|
|
48
|
+
WORKER_STATES.has(value.state) &&
|
|
49
|
+
(typeof value.pid === 'number' || value.pid === null) &&
|
|
50
|
+
typeof value.startedAt === 'string' &&
|
|
51
|
+
typeof value.updatedAt === 'string' &&
|
|
52
|
+
typeof value.pendingAtStart === 'number' &&
|
|
53
|
+
typeof value.processed === 'number' &&
|
|
54
|
+
typeof value.skipped === 'number' &&
|
|
55
|
+
typeof value.errors === 'number' &&
|
|
56
|
+
(value.lastError === undefined || typeof value.lastError === 'string'));
|
|
57
|
+
}
|
|
58
|
+
function readDigestWorkerStatus(workspaceRoot) {
|
|
59
|
+
try {
|
|
60
|
+
const parsed = JSON.parse(fs.readFileSync(digestWorkerStatusPath(workspaceRoot), 'utf8'));
|
|
61
|
+
return isDigestWorkerStatus(parsed) ? parsed : undefined;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function writeDigestWorkerStatus(workspaceRoot, status) {
|
|
68
|
+
atomicWriteJsonFile(digestWorkerStatusPath(workspaceRoot), status);
|
|
69
|
+
}
|
|
70
|
+
function appendDigestWorkerLog(workspaceRoot, message) {
|
|
71
|
+
try {
|
|
72
|
+
const logPath = digestWorkerLogPath(workspaceRoot);
|
|
73
|
+
fs.mkdirSync(path.dirname(logPath), { recursive: true });
|
|
74
|
+
fs.appendFileSync(logPath, `[${isoNow()}] ${message}\n`, 'utf8');
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Diagnostics must never block hook startup or worker completion.
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
function isLivePid(pid) {
|
|
81
|
+
if (pid === null || !Number.isInteger(pid) || pid <= 0)
|
|
82
|
+
return false;
|
|
83
|
+
try {
|
|
84
|
+
process.kill(pid, 0);
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
return err.code !== 'ESRCH';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function readLockPid(workspaceRoot) {
|
|
92
|
+
const lockPath = digestWorkerLockPath(workspaceRoot);
|
|
93
|
+
if (!fs.existsSync(lockPath))
|
|
94
|
+
return undefined;
|
|
95
|
+
try {
|
|
96
|
+
const parsed = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
97
|
+
return isRecord(parsed) && typeof parsed.pid === 'number' ? parsed.pid : null;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function statusFromPendingResult(result, startedAt, pendingAtStart) {
|
|
104
|
+
const updatedAt = isoNow();
|
|
105
|
+
if (!result.ok) {
|
|
106
|
+
return {
|
|
107
|
+
...emptyStatus('failed', pendingAtStart, startedAt),
|
|
108
|
+
updatedAt,
|
|
109
|
+
errors: 1,
|
|
110
|
+
lastError: result.error,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (result.action === 'digested') {
|
|
114
|
+
return {
|
|
115
|
+
state: 'complete',
|
|
116
|
+
pid: null,
|
|
117
|
+
startedAt,
|
|
118
|
+
updatedAt,
|
|
119
|
+
pendingAtStart,
|
|
120
|
+
processed: result.count,
|
|
121
|
+
skipped: result.skipped,
|
|
122
|
+
errors: result.errors,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
...emptyStatus(result.reason, pendingAtStart, startedAt),
|
|
127
|
+
updatedAt,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
export async function runDigestPendingWorker(input) {
|
|
131
|
+
const startedAt = isoNow();
|
|
132
|
+
let pendingAtStart = input.pendingAtStart ?? 0;
|
|
133
|
+
try {
|
|
134
|
+
const summary = summarizePendingConversations(input.workspaceRoot);
|
|
135
|
+
if (input.pendingAtStart === undefined) {
|
|
136
|
+
pendingAtStart = summary.initialized ? summary.pending : 0;
|
|
137
|
+
}
|
|
138
|
+
const runningStatus = {
|
|
139
|
+
...emptyStatus('running', pendingAtStart, startedAt),
|
|
140
|
+
pid: process.pid,
|
|
141
|
+
};
|
|
142
|
+
writeDigestWorkerStatus(input.workspaceRoot, runningStatus);
|
|
143
|
+
appendDigestWorkerLog(input.workspaceRoot, `worker started pid=${process.pid} pending=${pendingAtStart}`);
|
|
144
|
+
const result = await digestPendingConversations({
|
|
145
|
+
workspaceRoot: input.workspaceRoot,
|
|
146
|
+
digestFn: input.digestFn,
|
|
147
|
+
onProgress: (progress) => {
|
|
148
|
+
const currentStatus = {
|
|
149
|
+
...runningStatus,
|
|
150
|
+
updatedAt: isoNow(),
|
|
151
|
+
processed: progress.processed,
|
|
152
|
+
skipped: progress.skipped,
|
|
153
|
+
errors: progress.errors,
|
|
154
|
+
};
|
|
155
|
+
writeDigestWorkerStatus(input.workspaceRoot, currentStatus);
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
const status = statusFromPendingResult(result, startedAt, pendingAtStart);
|
|
159
|
+
writeDigestWorkerStatus(input.workspaceRoot, status);
|
|
160
|
+
appendDigestWorkerLog(input.workspaceRoot, `worker finished state=${status.state} processed=${status.processed} skipped=${status.skipped} errors=${status.errors}`);
|
|
161
|
+
return status;
|
|
162
|
+
}
|
|
163
|
+
catch (err) {
|
|
164
|
+
const status = {
|
|
165
|
+
...emptyStatus('failed', pendingAtStart, startedAt),
|
|
166
|
+
updatedAt: isoNow(),
|
|
167
|
+
errors: 1,
|
|
168
|
+
lastError: errorMessage(err),
|
|
169
|
+
};
|
|
170
|
+
writeDigestWorkerStatus(input.workspaceRoot, status);
|
|
171
|
+
appendDigestWorkerLog(input.workspaceRoot, `worker failed error=${status.lastError}`);
|
|
172
|
+
return status;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
export function launchDigestPendingWorker(input) {
|
|
176
|
+
const startedAt = isoNow();
|
|
177
|
+
let pendingAtStart = 0;
|
|
178
|
+
try {
|
|
179
|
+
const previousStatus = readDigestWorkerStatus(input.workspaceRoot);
|
|
180
|
+
if (previousStatus?.state === 'running' && isLivePid(previousStatus.pid)) {
|
|
181
|
+
const status = {
|
|
182
|
+
...previousStatus,
|
|
183
|
+
state: 'locked',
|
|
184
|
+
updatedAt: isoNow(),
|
|
185
|
+
lastError: undefined,
|
|
186
|
+
};
|
|
187
|
+
writeDigestWorkerStatus(input.workspaceRoot, status);
|
|
188
|
+
return { ok: true, mode: 'background-launcher', status, spawned: false, reason: 'locked' };
|
|
189
|
+
}
|
|
190
|
+
const lockPid = readLockPid(input.workspaceRoot);
|
|
191
|
+
if (lockPid !== undefined && isLivePid(lockPid)) {
|
|
192
|
+
const status = emptyStatus('locked', 0, startedAt);
|
|
193
|
+
writeDigestWorkerStatus(input.workspaceRoot, status);
|
|
194
|
+
return { ok: true, mode: 'background-launcher', status, spawned: false, reason: 'locked' };
|
|
195
|
+
}
|
|
196
|
+
const summary = summarizePendingConversations(input.workspaceRoot);
|
|
197
|
+
if (!summary.initialized) {
|
|
198
|
+
const status = emptyStatus('not-initialized', 0, startedAt);
|
|
199
|
+
writeDigestWorkerStatus(input.workspaceRoot, status);
|
|
200
|
+
return { ok: true, mode: 'background-launcher', status, spawned: false, reason: 'not-initialized' };
|
|
201
|
+
}
|
|
202
|
+
if (summary.pending === 0) {
|
|
203
|
+
const status = emptyStatus('no-pending', 0, startedAt);
|
|
204
|
+
writeDigestWorkerStatus(input.workspaceRoot, status);
|
|
205
|
+
return { ok: true, mode: 'background-launcher', status, spawned: false, reason: 'no-pending' };
|
|
206
|
+
}
|
|
207
|
+
pendingAtStart = summary.pending;
|
|
208
|
+
if (!input.entrypoint) {
|
|
209
|
+
const status = {
|
|
210
|
+
...emptyStatus('failed', pendingAtStart, startedAt),
|
|
211
|
+
errors: 1,
|
|
212
|
+
lastError: 'CLI entrypoint is unavailable',
|
|
213
|
+
};
|
|
214
|
+
writeDigestWorkerStatus(input.workspaceRoot, status);
|
|
215
|
+
return {
|
|
216
|
+
ok: false,
|
|
217
|
+
mode: 'background-launcher',
|
|
218
|
+
status,
|
|
219
|
+
spawned: false,
|
|
220
|
+
reason: 'spawn-failed',
|
|
221
|
+
error: status.lastError,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
const child = spawn(process.execPath, [input.entrypoint, 'memory', 'digest-pending', ...input.args, `--${DIGEST_WORKER_FLAG}`], {
|
|
225
|
+
cwd: input.workspaceRoot,
|
|
226
|
+
detached: true,
|
|
227
|
+
stdio: 'ignore',
|
|
228
|
+
});
|
|
229
|
+
child.unref();
|
|
230
|
+
if (!child.pid) {
|
|
231
|
+
throw new Error('worker process did not expose a pid');
|
|
232
|
+
}
|
|
233
|
+
const status = {
|
|
234
|
+
...emptyStatus('running', pendingAtStart, startedAt),
|
|
235
|
+
pid: child.pid,
|
|
236
|
+
};
|
|
237
|
+
writeDigestWorkerStatus(input.workspaceRoot, status);
|
|
238
|
+
appendDigestWorkerLog(input.workspaceRoot, `launcher spawned pid=${child.pid} pending=${pendingAtStart}`);
|
|
239
|
+
return { ok: true, mode: 'background-launcher', status, spawned: true, pid: child.pid };
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
const status = {
|
|
243
|
+
...emptyStatus('failed', pendingAtStart, startedAt),
|
|
244
|
+
errors: 1,
|
|
245
|
+
lastError: errorMessage(err),
|
|
246
|
+
};
|
|
247
|
+
writeDigestWorkerStatus(input.workspaceRoot, status);
|
|
248
|
+
appendDigestWorkerLog(input.workspaceRoot, `launcher failed error=${status.lastError}`);
|
|
249
|
+
return {
|
|
250
|
+
ok: false,
|
|
251
|
+
mode: 'background-launcher',
|
|
252
|
+
status,
|
|
253
|
+
spawned: false,
|
|
254
|
+
reason: 'spawn-failed',
|
|
255
|
+
error: status.lastError,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { DigestModelId } from './types.js';
|
|
2
|
+
export declare const DEFAULT_DIGEST_MODEL_ID = DigestModelId.DEFAULT;
|
|
3
|
+
export declare const DEFAULT_DIGEST_MAX_INPUT_CHARS = 3000;
|
|
4
|
+
export declare const DEFAULT_DIGEST_TIMEOUT_MS = 120000;
|
|
5
|
+
export declare const DIGEST_TIMEOUT_GRACE_MS = 5000;
|
|
6
|
+
export declare const DIGEST_WORKER_FLAG = "__agent-kit-digest-worker";
|
|
7
|
+
export declare const DIGEST_WORKER_RESULT_PREFIX = "AGENT_KIT_DIGEST_RESULT ";
|
|
8
|
+
export declare const KIT_DIR = ".agent-kit";
|
|
9
|
+
export declare const WIKI_DIR: string;
|
|
10
|
+
export declare const WIKI_RAW_DIR: string;
|
|
11
|
+
export declare const PROVISIONAL_DIGEST_DIR: string;
|
|
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;
|
|
15
|
+
export declare const LLAMA_CONTEXT_SIZE = 4096;
|
|
16
|
+
export declare const LLAMA_MAX_GENERATED_TOKENS = 512;
|
|
17
|
+
export declare const LLAMA_TEMPERATURE = 0.1;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as path from 'node:path';
|
|
2
|
+
import { DigestModelId } from './types.js';
|
|
3
|
+
export const DEFAULT_DIGEST_MODEL_ID = DigestModelId.DEFAULT;
|
|
4
|
+
export const DEFAULT_DIGEST_MAX_INPUT_CHARS = 3000;
|
|
5
|
+
export const DEFAULT_DIGEST_TIMEOUT_MS = 120_000;
|
|
6
|
+
export const DIGEST_TIMEOUT_GRACE_MS = 5_000;
|
|
7
|
+
export const DIGEST_WORKER_FLAG = '__agent-kit-digest-worker';
|
|
8
|
+
export const DIGEST_WORKER_RESULT_PREFIX = 'AGENT_KIT_DIGEST_RESULT ';
|
|
9
|
+
export const KIT_DIR = '.agent-kit';
|
|
10
|
+
export const WIKI_DIR = path.join(KIT_DIR, 'wiki');
|
|
11
|
+
export const WIKI_RAW_DIR = path.join(WIKI_DIR, 'raw');
|
|
12
|
+
export const PROVISIONAL_DIGEST_DIR = path.join(WIKI_DIR, 'compiled', 'provisional', 'conversation-digests');
|
|
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');
|
|
17
|
+
export const LLAMA_CONTEXT_SIZE = 4096;
|
|
18
|
+
export const LLAMA_MAX_GENERATED_TOKENS = 512;
|
|
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
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
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 { initializeConversationDigestModel } from './processor.js';
|
|
7
|
+
import { DigestModelId } from './types.js';
|
|
8
|
+
const tempDirs = createTempDirTracker();
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
tempDirs.cleanup();
|
|
11
|
+
});
|
|
12
|
+
describe('initializeConversationDigestModel', () => {
|
|
13
|
+
test('rejects unknown model and leaves settings absent', async () => {
|
|
14
|
+
const workspace = tempDirs.makeTempDir('digest-init-');
|
|
15
|
+
const result = await initializeConversationDigestModel({
|
|
16
|
+
workspaceRoot: workspace,
|
|
17
|
+
modelId: 'unknown-model',
|
|
18
|
+
allowDownload: true,
|
|
19
|
+
});
|
|
20
|
+
assert.equal(result.initialized, false);
|
|
21
|
+
assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'settings.json')), false);
|
|
22
|
+
});
|
|
23
|
+
test('requires explicit download permission and leaves settings absent', async () => {
|
|
24
|
+
const workspace = tempDirs.makeTempDir('digest-init-');
|
|
25
|
+
const result = await initializeConversationDigestModel({
|
|
26
|
+
workspaceRoot: workspace,
|
|
27
|
+
modelId: DigestModelId.DEFAULT,
|
|
28
|
+
allowDownload: false,
|
|
29
|
+
});
|
|
30
|
+
assert.equal(result.initialized, false);
|
|
31
|
+
assert.equal(fs.existsSync(path.join(workspace, '.agent-kit', 'settings.json')), false);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as assert from 'node:assert/strict';
|
|
2
|
+
import { describe, test } from 'node:test';
|
|
3
|
+
import { createLlamaLocalDigestProvider } 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('runs an opt-in smoke test against a real model', { skip: !process.env.AGENT_KIT_DIGEST_MODEL_ID }, async () => {
|
|
11
|
+
const modelId = process.env.AGENT_KIT_DIGEST_MODEL_ID;
|
|
12
|
+
const provider = await createLlamaLocalDigestProvider(modelId);
|
|
13
|
+
const output = await provider.generateDigestMarkdown(INPUT, {
|
|
14
|
+
modelId,
|
|
15
|
+
maxInputChars: 1000,
|
|
16
|
+
timeoutMs: 120_000,
|
|
17
|
+
});
|
|
18
|
+
assert.match(output, /^# Conversation Digest:/);
|
|
19
|
+
assert.match(output, /Status: provisional/);
|
|
20
|
+
});
|
|
21
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|