open-context-engine 0.1.2 → 0.1.4
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/README.md +4 -2
- package/README.zh-CN.md +4 -2
- package/bin/opencontextengine.mjs +2 -2
- package/docs/QUICKSTART.md +39 -7
- package/package.json +5 -3
- package/scripts/retrieval-server.py +72 -15
- package/src/client.mjs +5 -1
- package/src/config.mjs +1 -1
- package/src/eval/remote-models.mjs +1 -1
- package/src/mcp.mjs +4 -2
- package/src/retrieval/batched.py +1 -1
- package/src/retrieval/cascade.py +1 -1
- package/src/retrieval/engine.py +2 -2
- package/src/retrieval/entities.py +1 -1
- package/src/retrieval/evidence.py +312 -0
- package/src/retrieval/languages/__init__.py +33 -4
- package/src/retrieval/languages/go.py +10 -6
- package/src/retrieval/languages/go_ast.go +11 -0
- package/src/retrieval/languages/go_types.go +2 -0
- package/src/retrieval/languages/python.py +23 -13
- package/src/retrieval/languages/python_calls.py +188 -0
- package/src/retrieval/languages/schema.py +9 -2
- package/src/retrieval/languages/typescript.mjs +14 -6
- package/src/retrieval/languages/typescript.py +4 -1
- package/src/retrieval/live.py +13 -12
- package/src/retrieval/planning.py +15 -0
- package/src/retrieval/reranker.py +12 -6
- package/src/retrieval/shared_worker.py +115 -0
- package/src/retrieval/writer_lock.py +29 -0
- package/src/runtime.mjs +9 -2
- package/src/service.mjs +26 -8
- package/src/setup.mjs +8 -5
- package/src/shared-service.mjs +142 -0
- package/src/workspaces.mjs +8 -6
package/src/service.mjs
CHANGED
|
@@ -5,21 +5,22 @@ import { homedir } from 'node:os';
|
|
|
5
5
|
import { resolve, dirname, delimiter } from 'node:path';
|
|
6
6
|
import { createInterface } from 'node:readline';
|
|
7
7
|
import { projectRoot, loadEnvironment } from './config.mjs';
|
|
8
|
+
import { defaultPython, venvPython } from './runtime.mjs';
|
|
8
9
|
import { embeddingTransportConfig, remoteRerankerConfig, rerankerExecutionTransport } from './eval/remote-models.mjs';
|
|
9
10
|
|
|
10
11
|
export function serviceConfig({root, state, port = 0} = {}, environment = process.env) {
|
|
11
12
|
const env = loadEnvironment(environment);
|
|
12
13
|
const embedding = embeddingTransportConfig(env);
|
|
13
14
|
const reranker = remoteRerankerConfig(env), runtime = rerankerExecutionTransport(env);
|
|
14
|
-
const repository = root ? realpathSync(resolve(root)) : undefined;
|
|
15
|
+
const repository = root ? realpathSync.native(resolve(root)) : undefined;
|
|
15
16
|
const workspaceId = repository && createHash('sha256').update(repository).digest('hex').slice(0, 24);
|
|
16
|
-
const candidates = ['.venv
|
|
17
|
-
'.pilot-state/language-adapters-venv
|
|
17
|
+
const candidates = ['.venv', '.pilot-state/baselines/cocoindex-venv',
|
|
18
|
+
'.pilot-state/language-adapters-venv'].map(path => venvPython(resolve(projectRoot, path)));
|
|
18
19
|
const currentState = repository && resolve(homedir(), '.cache/opencontextengine', workspaceId);
|
|
19
20
|
const previousState = repository && resolve(homedir(), '.cache/reponerve', workspaceId);
|
|
20
21
|
const defaultState = repository && (existsSync(currentState) ? currentState : existsSync(previousState) ? previousState : currentState);
|
|
21
22
|
return {
|
|
22
|
-
python: env.OCE_PYTHON || candidates.find(existsSync) ||
|
|
23
|
+
python: env.OCE_PYTHON || candidates.find(existsSync) || defaultPython(),
|
|
23
24
|
workerEnv: {...(env.OCE_GO_BINARY ? {OCE_GO_BINARY:env.OCE_GO_BINARY} : {}),
|
|
24
25
|
...(env.TIKTOKEN_CACHE_DIR ? {TIKTOKEN_CACHE_DIR:env.TIKTOKEN_CACHE_DIR} : {})},
|
|
25
26
|
config: {root: repository, state: state ? resolve(state) : repository
|
|
@@ -29,6 +30,7 @@ export function serviceConfig({root, state, port = 0} = {}, environment = proces
|
|
|
29
30
|
embeddingKey: env.EMBEDDING_API_KEY,
|
|
30
31
|
embeddingModel: env.EMBEDDING_MODEL || 'Qwen3-Embedding-4B',
|
|
31
32
|
embeddingDimensions: Number(env.OCE_EMBEDDING_DIMENSIONS || 1024),
|
|
33
|
+
embeddingBatchSize: Number(env.OCE_EMBEDDING_BATCH_SIZE ?? 64),
|
|
32
34
|
embeddingRevision: env.OCE_EMBEDDING_REVISION || '1',
|
|
33
35
|
reranker: {...reranker, baseUrl: runtime.requestBaseUrl},
|
|
34
36
|
languageOptions: env.OCE_LANGUAGE_OPTIONS ? JSON.parse(env.OCE_LANGUAGE_OPTIONS) : {},
|
|
@@ -37,12 +39,13 @@ export function serviceConfig({root, state, port = 0} = {}, environment = proces
|
|
|
37
39
|
};
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
export function startService(settings, {log = line => process.stderr.write(line + '\n')} = {}) {
|
|
42
|
+
export function startService(settings, {log = line => process.stderr.write(line + '\n'), startupMs = 15000} = {}) {
|
|
41
43
|
const {python, config} = settings;
|
|
42
44
|
const child = spawn(python, [resolve(projectRoot, 'scripts/retrieval-server.py')], {
|
|
43
45
|
cwd: projectRoot, env: {...process.env, ...settings.workerEnv,
|
|
44
|
-
PATH:dirname(process.execPath)+delimiter+(process.env.PATH || ''),
|
|
45
|
-
|
|
46
|
+
PATH:dirname(process.execPath)+delimiter+(process.env.PATH || ''),
|
|
47
|
+
PYTHONUTF8:'1', PYTHONIOENCODING:'utf-8', OPENBLAS_NUM_THREADS:'2', OMP_NUM_THREADS:'2'},
|
|
48
|
+
stdio:['pipe', 'pipe', 'pipe'], detached:Boolean(config.shared), windowsHide:true,
|
|
46
49
|
});
|
|
47
50
|
const lines = createInterface({input: child.stdout});
|
|
48
51
|
const errors = createInterface({input: child.stderr});
|
|
@@ -50,14 +53,29 @@ export function startService(settings, {log = line => process.stderr.write(line
|
|
|
50
53
|
child.stdin.on('error', () => {}); // Spawn/exit handlers report early failures.
|
|
51
54
|
child.stdin.end(JSON.stringify(config) + '\n');
|
|
52
55
|
const ready = new Promise((resolveReady, reject) => {
|
|
53
|
-
const timer = setTimeout(() => {
|
|
56
|
+
const timer = setTimeout(() => {
|
|
57
|
+
child.kill();
|
|
58
|
+
reject(Object.assign(new Error('Retrieval worker startup timed out'), {code:'STARTUP_TIMEOUT'}));
|
|
59
|
+
}, startupMs);
|
|
54
60
|
child.once('error', error => {clearTimeout(timer); reject(error);});
|
|
55
61
|
child.once('exit', code => {clearTimeout(timer); reject(new Error(`Retrieval worker exited (${code})`));});
|
|
56
62
|
lines.on('line', line => {
|
|
57
63
|
try {
|
|
58
64
|
const value = JSON.parse(line);
|
|
65
|
+
if (value.startupError) {
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
const error = new Error(`Retrieval worker failed to start: ${value.startupError.message}`);
|
|
68
|
+
error.code = value.startupError.code;
|
|
69
|
+
reject(error);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
59
72
|
if (value.listening) {
|
|
60
73
|
clearTimeout(timer);
|
|
74
|
+
if (config.shared) {
|
|
75
|
+
lines.close(); errors.close();
|
|
76
|
+
child.stdout.destroy(); child.stderr.destroy();
|
|
77
|
+
child.unref();
|
|
78
|
+
}
|
|
61
79
|
resolveReady({baseUrl:value.listening, apiKey:config.serviceKey});
|
|
62
80
|
return;
|
|
63
81
|
}
|
package/src/setup.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { createInterface } from 'node:readline/promises';
|
|
|
2
2
|
import { Writable } from 'node:stream';
|
|
3
3
|
import { configDirectory, loadEnvironment, saveUserConfig } from './config.mjs';
|
|
4
4
|
import { remoteRerankerConfig, embeddingTransportConfig, rerankerExecutionTransport } from './eval/remote-models.mjs';
|
|
5
|
-
import { ensureRuntime, run } from './runtime.mjs';
|
|
5
|
+
import { ensureRuntime, run, defaultPython } from './runtime.mjs';
|
|
6
6
|
|
|
7
7
|
export function validateModels(env) {
|
|
8
8
|
embeddingTransportConfig(env);
|
|
@@ -13,6 +13,8 @@ export function validateModels(env) {
|
|
|
13
13
|
}
|
|
14
14
|
const dimensions = Number(env.OCE_EMBEDDING_DIMENSIONS);
|
|
15
15
|
if (!Number.isInteger(dimensions) || dimensions < 1) throw new Error('Embedding dimensions must be a positive integer');
|
|
16
|
+
const batchSize = Number(env.OCE_EMBEDDING_BATCH_SIZE ?? 64);
|
|
17
|
+
if (!Number.isInteger(batchSize) || batchSize < 1 || batchSize > 64) throw new Error('Embedding batch size must be an integer from 1 to 64');
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export function terminalPrompt(input = process.stdin, output = process.stderr) {
|
|
@@ -37,8 +39,9 @@ export function terminalPrompt(input = process.stdin, output = process.stderr) {
|
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
export async function setup({environment = process.env, nonInteractive = false, python,
|
|
40
|
-
prompt, install = ensureRuntime, execute = run,
|
|
41
|
-
|
|
42
|
+
platform = process.platform, prompt, install = ensureRuntime, execute = run,
|
|
43
|
+
log = message => process.stderr.write(message+'\n')} = {}) {
|
|
44
|
+
if (!['darwin','linux','win32'].includes(platform)) throw new Error('OpenContextEngine supports macOS, Linux and Windows');
|
|
42
45
|
let env = {EMBEDDING_MODEL:'Qwen3-Embedding-4B',RERANK_MODEL:'Qwen3-Reranker-4B',
|
|
43
46
|
OCE_EMBEDDING_DIMENSIONS:'1024',...loadEnvironment(environment)};
|
|
44
47
|
log(`Configuration: ${configDirectory(environment)}`);
|
|
@@ -57,10 +60,10 @@ export async function setup({environment = process.env, nonInteractive = false,
|
|
|
57
60
|
validateModels(env);
|
|
58
61
|
await execute('git',['--version'],{timeout:10000});
|
|
59
62
|
if (python) delete env.OCE_PYTHON;
|
|
60
|
-
const runtime = await install(env,{python:python ||
|
|
63
|
+
const runtime = await install(env,{platform,python:python || defaultPython(platform),execute,log});
|
|
61
64
|
env = {...env,...runtime};
|
|
62
65
|
const path = saveUserConfig(env,environment);
|
|
63
|
-
log(`Saved configuration to ${path}
|
|
66
|
+
log(`Saved configuration to ${path}.`);
|
|
64
67
|
log('Setup complete. Go projects additionally require Go 1.22+ on PATH or OCE_GO_BINARY.');
|
|
65
68
|
return {path,env};
|
|
66
69
|
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { createHash, createHmac, randomBytes, randomUUID } from 'node:crypto';
|
|
2
|
+
import { readFile, mkdir, realpath } from 'node:fs/promises';
|
|
3
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
6
|
+
import { projectRoot } from './config.mjs';
|
|
7
|
+
import { startService } from './service.mjs';
|
|
8
|
+
|
|
9
|
+
const protocol = 1;
|
|
10
|
+
function stable(value) {
|
|
11
|
+
if (Array.isArray(value)) return value.map(stable);
|
|
12
|
+
if (value && typeof value === 'object') return Object.fromEntries(Object.keys(value).sort()
|
|
13
|
+
.filter(key => value[key] !== undefined).map(key => [key, stable(value[key])]));
|
|
14
|
+
return value;
|
|
15
|
+
}
|
|
16
|
+
function runtimeIdentity() {
|
|
17
|
+
const hash = createHash('sha256');
|
|
18
|
+
function add(path) {
|
|
19
|
+
for (const entry of readdirSync(join(projectRoot, path), {withFileTypes:true}).sort((a,b) => a.name.localeCompare(b.name))) {
|
|
20
|
+
const name = `${path}/${entry.name}`;
|
|
21
|
+
if (entry.isDirectory() && entry.name !== '__pycache__') add(name);
|
|
22
|
+
else if (entry.isFile() && /\.(py|mjs|go)$/.test(name)) hash.update(name).update(readFileSync(join(projectRoot, name)));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
add('src/retrieval');
|
|
26
|
+
for (const name of ['scripts/retrieval-server.py', 'requirements.txt']) hash.update(readFileSync(join(projectRoot, name)));
|
|
27
|
+
hash.update(JSON.parse(readFileSync(join(projectRoot, 'package.json'))).version);
|
|
28
|
+
return hash.digest('hex');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// A handle owns a renewable lease, never the lifetime of the shared child process.
|
|
32
|
+
export function startSharedService(settings, {startupMs = 15000, leaseSeconds = 15, idleSeconds = 30, start = startService} = {}) {
|
|
33
|
+
const runtime = runtimeIdentity();
|
|
34
|
+
let configured, connection, pending, timer, closed = false, closePromise;
|
|
35
|
+
const leaseId = randomUUID();
|
|
36
|
+
const initialized = (async () => {
|
|
37
|
+
await mkdir(settings.config.state, {recursive:true, mode:0o700});
|
|
38
|
+
const root = await realpath(settings.config.root), state = await realpath(settings.config.state);
|
|
39
|
+
const config = {...settings.config, root, state};
|
|
40
|
+
const {serviceKey, port, shared, ...behavior} = config;
|
|
41
|
+
const identity = JSON.stringify(stable({protocol, runtime, config:behavior,
|
|
42
|
+
python:settings.python, workerEnv:settings.workerEnv || {}}));
|
|
43
|
+
configured = {config, identity, record:join(state, 'worker.json')};
|
|
44
|
+
})();
|
|
45
|
+
|
|
46
|
+
function fingerprint(key) {
|
|
47
|
+
// Credentials participate in compatibility without a reusable plaintext/hash in discovery files.
|
|
48
|
+
return createHmac('sha256', key).update(configured.identity).digest('hex');
|
|
49
|
+
}
|
|
50
|
+
async function readRecord() {
|
|
51
|
+
let record;
|
|
52
|
+
try {record = JSON.parse(await readFile(configured.record, 'utf8'));}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (error.code === 'ENOENT' || error instanceof SyntaxError) return;
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
if (!record || !Number.isInteger(record.port) || record.port < 1 || record.port > 65535 ||
|
|
58
|
+
typeof record.apiKey !== 'string' || !/^[a-f0-9]{64}$/.test(record.apiKey) ||
|
|
59
|
+
typeof record.instanceId !== 'string') return;
|
|
60
|
+
// Discovery cannot redirect credentials or requests off this machine.
|
|
61
|
+
return {...record, baseUrl:`http://127.0.0.1:${record.port}`};
|
|
62
|
+
}
|
|
63
|
+
async function lease(record, release = false) {
|
|
64
|
+
const response = await fetch(record.baseUrl + (release ? '/release' : '/lease'), {
|
|
65
|
+
method:'POST', redirect:'error', signal:AbortSignal.timeout(1500),
|
|
66
|
+
headers:{'content-type':'application/json', authorization:`Bearer ${record.apiKey}`},
|
|
67
|
+
body:JSON.stringify({instanceId:record.instanceId, fingerprint:fingerprint(record.apiKey), leaseId}),
|
|
68
|
+
});
|
|
69
|
+
const body = await response.json();
|
|
70
|
+
if (response.status === 409 && body.code === 'CONFIG_MISMATCH') {
|
|
71
|
+
throw Object.assign(new Error(body.error), {code:'CONFIG_MISMATCH'});
|
|
72
|
+
}
|
|
73
|
+
if (!response.ok || body.instanceId !== record.instanceId || record.protocol !== protocol ||
|
|
74
|
+
!Number.isFinite(body.leaseSeconds) || body.leaseSeconds <= 0) throw new Error('Shared worker handshake failed');
|
|
75
|
+
return {...record, leaseSeconds:body.leaseSeconds};
|
|
76
|
+
}
|
|
77
|
+
function schedule(record) {
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
timer = setTimeout(() => {void get().catch(() => {});}, record.leaseSeconds * 1000 / 3);
|
|
80
|
+
timer.unref();
|
|
81
|
+
}
|
|
82
|
+
async function connect() {
|
|
83
|
+
await initialized;
|
|
84
|
+
const deadline = performance.now() + startupMs;
|
|
85
|
+
let nextSpawn = 0, lastBusy;
|
|
86
|
+
while (!closed && performance.now() < deadline) {
|
|
87
|
+
const record = connection || await readRecord();
|
|
88
|
+
if (record) {
|
|
89
|
+
try {
|
|
90
|
+
connection = await lease(record);
|
|
91
|
+
if (closed) {await lease(connection, true).catch(() => {}); break;}
|
|
92
|
+
schedule(connection);
|
|
93
|
+
return {baseUrl:connection.baseUrl, apiKey:connection.apiKey};
|
|
94
|
+
} catch (error) {
|
|
95
|
+
connection = undefined;
|
|
96
|
+
if (error.code === 'CONFIG_MISMATCH') throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (performance.now() >= nextSpawn) {
|
|
100
|
+
if (runtimeIdentity() !== runtime) throw new Error('Worker runtime changed on disk; restart this MCP client');
|
|
101
|
+
const key = randomBytes(32).toString('hex');
|
|
102
|
+
const worker = start({...settings, config:{...configured.config, port:0, serviceKey:key,
|
|
103
|
+
shared:{fingerprint:fingerprint(key), leaseSeconds, idleSeconds}}}, {log:() => {}, startupMs:Math.max(1, deadline - performance.now())});
|
|
104
|
+
try {await worker.ready;}
|
|
105
|
+
catch (error) {
|
|
106
|
+
await worker.close();
|
|
107
|
+
// A retry near the overall deadline may have too little time to import
|
|
108
|
+
// Python. Preserve the writer conflict already established by an earlier attempt.
|
|
109
|
+
if (error.code === 'STARTUP_TIMEOUT' && lastBusy) break;
|
|
110
|
+
if (error.code !== 'INDEX_LOCKED') throw error;
|
|
111
|
+
lastBusy = error;
|
|
112
|
+
}
|
|
113
|
+
nextSpawn = performance.now() + 1000;
|
|
114
|
+
}
|
|
115
|
+
await delay(100);
|
|
116
|
+
}
|
|
117
|
+
if (closed) throw new Error('MCP workspace manager is shutting down');
|
|
118
|
+
throw new Error(lastBusy
|
|
119
|
+
? `${lastBusy.message}. No compatible shared worker became available. An older or manually started worker may own this index; close its clients or connect to it explicitly.`
|
|
120
|
+
: 'Shared retrieval worker startup or connection timed out');
|
|
121
|
+
}
|
|
122
|
+
function get() {
|
|
123
|
+
if (closed) return Promise.reject(new Error('MCP workspace manager is shutting down'));
|
|
124
|
+
if (!pending) {
|
|
125
|
+
pending = connect().finally(() => {pending = undefined;});
|
|
126
|
+
}
|
|
127
|
+
return pending;
|
|
128
|
+
}
|
|
129
|
+
function close() {
|
|
130
|
+
if (!closePromise) {
|
|
131
|
+
closed = true;
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
closePromise = (async () => {
|
|
134
|
+
await pending?.catch(() => {});
|
|
135
|
+
if (connection) await lease(connection, true).catch(() => {});
|
|
136
|
+
connection = undefined;
|
|
137
|
+
})();
|
|
138
|
+
}
|
|
139
|
+
return closePromise;
|
|
140
|
+
}
|
|
141
|
+
return {ready:get(), get, close};
|
|
142
|
+
}
|
package/src/workspaces.mjs
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { realpathSync, statSync } from 'node:fs';
|
|
2
2
|
import { isAbsolute, resolve, join } from 'node:path';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
-
import { serviceConfig
|
|
4
|
+
import { serviceConfig } from './service.mjs';
|
|
5
|
+
import { startSharedService } from './shared-service.mjs';
|
|
5
6
|
|
|
6
7
|
function directory(path) {
|
|
7
|
-
const canonical = realpathSync(path);
|
|
8
|
+
const canonical = realpathSync.native(path);
|
|
8
9
|
if (!statSync(canonical).isDirectory()) throw new Error('directory_path must point to a directory');
|
|
9
10
|
return canonical;
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
// Each
|
|
13
|
-
export function createWorkspaceManager({root, state} = {}, {configure = serviceConfig, start =
|
|
13
|
+
// Each manager holds one lease per canonical repository, including while connecting.
|
|
14
|
+
export function createWorkspaceManager({root, state} = {}, {configure = serviceConfig, start = startSharedService} = {}) {
|
|
14
15
|
const fixedRoot = root ? directory(resolve(root)) : undefined;
|
|
15
16
|
const workers = new Map();
|
|
16
17
|
let closing = false, closed;
|
|
@@ -35,13 +36,14 @@ export function createWorkspaceManager({root, state} = {}, {configure = serviceC
|
|
|
35
36
|
entry = {worker};
|
|
36
37
|
workers.set(repository, entry);
|
|
37
38
|
const remove = () => {if (workers.get(repository) === entry) workers.delete(repository);};
|
|
38
|
-
worker.child
|
|
39
|
+
worker.child?.once('exit', remove);
|
|
39
40
|
entry.ready = worker.ready.catch(async error => {
|
|
40
41
|
try {await worker.close();} finally {remove();}
|
|
41
42
|
throw error;
|
|
42
43
|
});
|
|
43
44
|
}
|
|
44
|
-
|
|
45
|
+
await entry.ready;
|
|
46
|
+
return entry.worker.get ? entry.worker.get() : entry.ready;
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
function close() {
|