neuralos 3.8.8 → 3.9.0
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/bin/gybackend.cjs +108 -3
- package/package.json +21 -21
- package/plugins/neuralos/index.mjs +131 -0
- package/plugins/neuralos/neuralos.extreme.spec.mts +289 -0
- package/plugins/neuralos/neuralosEngine.mjs +299 -0
- package/plugins/neuralos/plugin.json +15 -0
package/bin/gybackend.cjs
CHANGED
|
@@ -346328,6 +346328,7 @@ var AgentRunLedger = class {
|
|
|
346328
346328
|
var CHECKPOINT_STRING_CAP = 8 * 1024;
|
|
346329
346329
|
var CHECKPOINT_BYTES_CAP = 16 * 1024 * 1024;
|
|
346330
346330
|
var CHECKPOINT_MAX_MESSAGES = 40;
|
|
346331
|
+
var CHECKPOINT_RETENTION = 8;
|
|
346331
346332
|
function truncateString(input, max = CHECKPOINT_STRING_CAP) {
|
|
346332
346333
|
if (input.length <= max) return input;
|
|
346333
346334
|
const omitted = input.length - max;
|
|
@@ -346395,8 +346396,18 @@ function isAllocationError(error40) {
|
|
|
346395
346396
|
var SafeMemorySaver = class extends MemorySaver {
|
|
346396
346397
|
async put(config2, checkpoint, metadata) {
|
|
346397
346398
|
const pruned = pruneCheckpointForSaver(checkpoint);
|
|
346399
|
+
const threadId = config2?.configurable?.thread_id;
|
|
346400
|
+
const ns = config2?.configurable?.checkpoint_ns ?? "";
|
|
346401
|
+
const checkpointId = checkpoint?.id;
|
|
346402
|
+
if (threadId && checkpointId) {
|
|
346403
|
+
const order = this.putOrderFor(threadId, ns);
|
|
346404
|
+
order.push(checkpointId);
|
|
346405
|
+
this.setPutOrder(threadId, ns, order.slice(-CHECKPOINT_RETENTION * 4));
|
|
346406
|
+
}
|
|
346398
346407
|
try {
|
|
346399
|
-
|
|
346408
|
+
const result = await super.put(config2, pruned, pruneValue(metadata));
|
|
346409
|
+
this.evictOldCheckpoints(config2);
|
|
346410
|
+
return result;
|
|
346400
346411
|
} catch (error40) {
|
|
346401
346412
|
if (!isAllocationError(error40)) throw error40;
|
|
346402
346413
|
console.warn(
|
|
@@ -346409,12 +346420,15 @@ var SafeMemorySaver = class extends MemorySaver {
|
|
|
346409
346420
|
channels.messages = channels.messages.slice(-8);
|
|
346410
346421
|
}
|
|
346411
346422
|
tighter.channel_values = channels;
|
|
346412
|
-
|
|
346423
|
+
const result = await super.put(config2, tighter, { source: "oom-pruned" });
|
|
346424
|
+
this.evictOldCheckpoints(config2);
|
|
346425
|
+
return result;
|
|
346413
346426
|
} catch (retryError) {
|
|
346414
346427
|
if (!isAllocationError(retryError)) throw retryError;
|
|
346415
346428
|
console.warn(
|
|
346416
346429
|
"[SafeMemorySaver] checkpoint still too large \u2014 skipping persist so the run can finish"
|
|
346417
346430
|
);
|
|
346431
|
+
this.evictOldCheckpoints(config2);
|
|
346418
346432
|
return {
|
|
346419
346433
|
configurable: {
|
|
346420
346434
|
thread_id: config2?.configurable?.thread_id,
|
|
@@ -346425,6 +346439,46 @@ var SafeMemorySaver = class extends MemorySaver {
|
|
|
346425
346439
|
}
|
|
346426
346440
|
}
|
|
346427
346441
|
}
|
|
346442
|
+
/**
|
|
346443
|
+
* Keep only the newest CHECKPOINT_RETENTION checkpoints for the thread/ns
|
|
346444
|
+
* in this config. MemorySaver stores them as
|
|
346445
|
+
* storage[threadId][ns][checkpointId] = [serializedCheckpoint, metadata,
|
|
346446
|
+
* parentId]; there is no timestamp, but checkpoint ids from langgraph are
|
|
346447
|
+
* UUIDv4-orderable-ish in practice and — more reliably — the number of
|
|
346448
|
+
* entries is what matters for memory. We cannot know insertion order from
|
|
346449
|
+
* the object shape alone, so we track it ourselves: the ids we just put
|
|
346450
|
+
* are recorded, and anything beyond the newest window is dropped.
|
|
346451
|
+
*/
|
|
346452
|
+
evictOldCheckpoints(config2) {
|
|
346453
|
+
try {
|
|
346454
|
+
const threadId = config2?.configurable?.thread_id;
|
|
346455
|
+
const ns = config2?.configurable?.checkpoint_ns ?? "";
|
|
346456
|
+
if (!threadId) return;
|
|
346457
|
+
const storage = this.storage;
|
|
346458
|
+
const nsMap = storage?.[threadId]?.[ns];
|
|
346459
|
+
if (!nsMap) return;
|
|
346460
|
+
const ids = Object.keys(nsMap);
|
|
346461
|
+
if (ids.length <= CHECKPOINT_RETENTION) return;
|
|
346462
|
+
const order = this.putOrderFor(threadId, ns);
|
|
346463
|
+
const keep = new Set(order.slice(-CHECKPOINT_RETENTION));
|
|
346464
|
+
for (const id of ids) {
|
|
346465
|
+
if (!keep.has(id) && !order.includes(id)) keep.add(id);
|
|
346466
|
+
}
|
|
346467
|
+
for (const id of ids) {
|
|
346468
|
+
if (!keep.has(id)) delete nsMap[id];
|
|
346469
|
+
}
|
|
346470
|
+
const remaining = order.filter((id) => id in nsMap);
|
|
346471
|
+
this.setPutOrder(threadId, ns, remaining.slice(-CHECKPOINT_RETENTION * 2));
|
|
346472
|
+
} catch {
|
|
346473
|
+
}
|
|
346474
|
+
}
|
|
346475
|
+
putOrders = /* @__PURE__ */ new Map();
|
|
346476
|
+
putOrderFor(threadId, ns) {
|
|
346477
|
+
return this.putOrders.get(`${threadId}\0${ns}`) ?? [];
|
|
346478
|
+
}
|
|
346479
|
+
setPutOrder(threadId, ns, ids) {
|
|
346480
|
+
this.putOrders.set(`${threadId}\0${ns}`, ids);
|
|
346481
|
+
}
|
|
346428
346482
|
async putWrites(config2, writes, taskId) {
|
|
346429
346483
|
const prunedWrites = Array.isArray(writes) ? writes.map((w) => {
|
|
346430
346484
|
if (Array.isArray(w) && w.length >= 2) {
|
|
@@ -371792,6 +371846,23 @@ ${reminder}`;
|
|
|
371792
371846
|
baseMessages = mapStoredMessagesToChatMessages(
|
|
371793
371847
|
sanitizedStoredMessages.messages
|
|
371794
371848
|
);
|
|
371849
|
+
const interruptedRun = this.chatHistoryService.getRunMarker(sessionId);
|
|
371850
|
+
if (interruptedRun) {
|
|
371851
|
+
const minutesAgo = Math.max(
|
|
371852
|
+
0,
|
|
371853
|
+
Math.round((Date.now() - (interruptedRun.startedAt || 0)) / 6e4)
|
|
371854
|
+
);
|
|
371855
|
+
baseMessages.push(
|
|
371856
|
+
new SystemMessage(
|
|
371857
|
+
`[INTERRUPTION NOTICE] The previous task on this session was interrupted ${minutesAgo > 0 ? `${minutesAgo} minutes ` : ""}ago (the application stopped before the response completed \u2014 likely a force quit or crash). The last assistant response above may be PARTIAL: it can end mid-sentence, mid-tool-call, or before the task was actually finished. ` + (interruptedRun.inputPreview ? `The interrupted task began with: "${interruptedRun.inputPreview}"
|
|
371858
|
+
` : "") + `Do not assume the prior task completed. If the user's next message relates to it, offer to continue or finish the interrupted work rather than starting from scratch.`
|
|
371859
|
+
)
|
|
371860
|
+
);
|
|
371861
|
+
this.chatHistoryService.clearRunMarker(sessionId);
|
|
371862
|
+
console.warn(
|
|
371863
|
+
`[AgentService_v2] Restored session ${sessionId} after an interrupted run (${interruptedRun.runId}); injected interruption notice into context.`
|
|
371864
|
+
);
|
|
371865
|
+
}
|
|
371795
371866
|
}
|
|
371796
371867
|
const runExperimentalFlags = resolveRunExperimentalFlags(
|
|
371797
371868
|
context2,
|
|
@@ -371811,6 +371882,11 @@ ${reminder}`;
|
|
|
371811
371882
|
execCommandActionModelEnabled: runExperimentalFlags.execCommandActionModelEnabled,
|
|
371812
371883
|
writeStdinActionModelEnabled: runExperimentalFlags.writeStdinActionModelEnabled
|
|
371813
371884
|
};
|
|
371885
|
+
this.chatHistoryService.setRunMarker(sessionId, {
|
|
371886
|
+
runId: ledgerRunId,
|
|
371887
|
+
startedAt: Date.now(),
|
|
371888
|
+
inputPreview: ledgerInputPreview.slice(0, 500)
|
|
371889
|
+
});
|
|
371814
371890
|
try {
|
|
371815
371891
|
const result = await this.graph.invoke(initialState, {
|
|
371816
371892
|
recursionLimit,
|
|
@@ -371869,6 +371945,7 @@ ${reminder}`;
|
|
|
371869
371945
|
this.currentRunIdBySession.delete(sessionId);
|
|
371870
371946
|
this.agentRunLedger?.finishRun(ledgerRunId, ledgerExitStatus, ledgerExitError);
|
|
371871
371947
|
void this.recordCompoundingLessons(sessionId, ledgerRunId, ledgerExitStatus, ledgerExitError);
|
|
371948
|
+
this.chatHistoryService.clearRunMarker(sessionId);
|
|
371872
371949
|
await this.clearCheckpoint(sessionId);
|
|
371873
371950
|
}
|
|
371874
371951
|
}
|
|
@@ -373440,9 +373517,11 @@ var ChatHistoryService = class {
|
|
|
373440
373517
|
saveSession(session) {
|
|
373441
373518
|
const createdAt = this.store.getChatSessionCreatedAt(session.id);
|
|
373442
373519
|
const now = Date.now();
|
|
373520
|
+
const storedMeta = this.store.getChatSessionMeta(session.id);
|
|
373521
|
+
const title = storedMeta?.title ?? session.title;
|
|
373443
373522
|
this.store.saveChatSession({
|
|
373444
373523
|
id: session.id,
|
|
373445
|
-
title
|
|
373524
|
+
title,
|
|
373446
373525
|
messages: Array.from(session.messages.entries()).map(([id, message]) => ({
|
|
373447
373526
|
id,
|
|
373448
373527
|
type: message._getType ? message._getType() : "unknown",
|
|
@@ -373462,6 +373541,32 @@ var ChatHistoryService = class {
|
|
|
373462
373541
|
getSessionMeta(sessionId) {
|
|
373463
373542
|
return this.store.getChatSessionMeta(sessionId);
|
|
373464
373543
|
}
|
|
373544
|
+
// ─── v3.8.9: run interruption markers ─────────────────────────────────────
|
|
373545
|
+
// A marker is SET at run start and CLEARED in the run's finally block.
|
|
373546
|
+
// Only a hard kill (force quit, crash, power loss) skips the finally —
|
|
373547
|
+
// so a leftover marker is a durable, restart-surviving record that the
|
|
373548
|
+
// session's previous run was interrupted. Stored in the history_meta
|
|
373549
|
+
// table (plain SQLite, survives anything short of disk loss).
|
|
373550
|
+
runMarkerKey(sessionId) {
|
|
373551
|
+
return `run-marker:${sessionId}`;
|
|
373552
|
+
}
|
|
373553
|
+
setRunMarker(sessionId, marker) {
|
|
373554
|
+
this.store.setMeta(this.runMarkerKey(sessionId), JSON.stringify(marker));
|
|
373555
|
+
}
|
|
373556
|
+
getRunMarker(sessionId) {
|
|
373557
|
+
const raw = this.store.getMeta(this.runMarkerKey(sessionId));
|
|
373558
|
+
if (!raw) return null;
|
|
373559
|
+
try {
|
|
373560
|
+
const parsed = JSON.parse(raw);
|
|
373561
|
+
if (typeof parsed?.runId === "string") return parsed;
|
|
373562
|
+
return null;
|
|
373563
|
+
} catch {
|
|
373564
|
+
return null;
|
|
373565
|
+
}
|
|
373566
|
+
}
|
|
373567
|
+
clearRunMarker(sessionId) {
|
|
373568
|
+
this.store.setMeta(this.runMarkerKey(sessionId), "");
|
|
373569
|
+
}
|
|
373465
373570
|
loadSession(sessionId) {
|
|
373466
373571
|
const storedSession = this.store.loadChatSession(sessionId);
|
|
373467
373572
|
if (!storedSession) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
"
|
|
4
|
-
|
|
2
|
+
"main": "bin/gybackend.cjs",
|
|
3
|
+
"bin": {
|
|
4
|
+
"gybackend": "bin/gybackend.cjs"
|
|
5
|
+
},
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=18"
|
|
8
|
+
},
|
|
9
|
+
"dependencies": {
|
|
10
|
+
"@nats-io/jetstream": "^3.4.0",
|
|
11
|
+
"@nats-io/kv": "^3.4.0",
|
|
12
|
+
"@nats-io/transport-node": "^3.4.0",
|
|
13
|
+
"better-sqlite3": "^12.11.1"
|
|
14
|
+
},
|
|
15
|
+
"optionalDependencies": {
|
|
16
|
+
"node-pty": "1.2.0-beta.3",
|
|
17
|
+
"serialport": "^12.0.0",
|
|
18
|
+
"ssh2": "^1.17.0"
|
|
19
|
+
},
|
|
5
20
|
"keywords": [
|
|
6
21
|
"forward-deployed-engineer",
|
|
7
22
|
"fde",
|
|
@@ -34,22 +49,7 @@
|
|
|
34
49
|
"type": "git",
|
|
35
50
|
"url": "git+https://github.com/DrOlu/RTerm.git"
|
|
36
51
|
},
|
|
37
|
-
"
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
"bin": {
|
|
41
|
-
"gybackend": "bin/gybackend.cjs"
|
|
42
|
-
},
|
|
43
|
-
"main": "bin/gybackend.cjs",
|
|
44
|
-
"dependencies": {
|
|
45
|
-
"@nats-io/jetstream": "^3.4.0",
|
|
46
|
-
"@nats-io/kv": "^3.4.0",
|
|
47
|
-
"@nats-io/transport-node": "^3.4.0",
|
|
48
|
-
"better-sqlite3": "^12.11.1"
|
|
49
|
-
},
|
|
50
|
-
"optionalDependencies": {
|
|
51
|
-
"serialport": "^12.0.0",
|
|
52
|
-
"ssh2": "^1.17.0",
|
|
53
|
-
"node-pty": "1.2.0-beta.3"
|
|
54
|
-
}
|
|
52
|
+
"name": "neuralos",
|
|
53
|
+
"version": "3.9.0",
|
|
54
|
+
"description": "Standalone neuralOS backend (rterm-backend): AI-native terminal & agentic-AI operations platform. Ships the neuralOS plugin — on-device data agents (verified probe menus over real data sources, engine + weights auto-provisioned on first use)."
|
|
55
55
|
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* neuralos — neuralOS instances as RTerm agent tools.
|
|
3
|
+
*
|
|
4
|
+
* neuralOS instances (~/neuralos-instances) are on-device data agents: a
|
|
5
|
+
* verified menu of probes over a real data source (databases, APIs, log
|
|
6
|
+
* sets), selected by the 121M neuralOS engine and executed by each
|
|
7
|
+
* instance's Python bridge. This plugin wires four agent tools:
|
|
8
|
+
*
|
|
9
|
+
* neuralos_list_instances — what can be queried
|
|
10
|
+
* neuralos_ask — plain-English question -> verified digest
|
|
11
|
+
* neuralos_graph — relationship maps (overview/neighbors/connect)
|
|
12
|
+
* neuralos_admin — write probes, confirm='yes' interlocked
|
|
13
|
+
*
|
|
14
|
+
* The engine ships with the RTerm desktop bundle ({resourcesPath}/neuralos);
|
|
15
|
+
* the standalone backend auto-provisions ~/.cache/neuralos on first use
|
|
16
|
+
* (one ~36 MB fetch from HuggingFace, offline forever after).
|
|
17
|
+
*
|
|
18
|
+
* Config (settings.neuralos, or env NEURALOS_*):
|
|
19
|
+
* instancesDir — instances root (default ~/neuralos-instances)
|
|
20
|
+
* pythonBin — interpreter with pydantic for the bridges (default python3)
|
|
21
|
+
* engineBin / engineWeights — explicit engine override
|
|
22
|
+
* autoDownload — provision the shared cache when nothing local (default true)
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { resolveConfig, listInstances, instanceDirFor, resolveEngine, engineSelect, executeProbe, graphProbe, adminProbe, truncate, defaultExec } from './neuralosEngine.mjs'
|
|
26
|
+
|
|
27
|
+
export { resolveConfig } from './neuralosEngine.mjs'
|
|
28
|
+
|
|
29
|
+
export function register(ctx) {
|
|
30
|
+
const cfg = resolveConfig(ctx)
|
|
31
|
+
const exec = defaultExec()
|
|
32
|
+
const log = (line) => {
|
|
33
|
+
try {
|
|
34
|
+
if (typeof ctx.log === 'function') ctx.log(`[neuralos] ${line}`)
|
|
35
|
+
} catch { /* logging must never break a tool */ }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
registerList(ctx, cfg, log)
|
|
39
|
+
registerAsk(ctx, cfg, exec, log)
|
|
40
|
+
registerGraph(ctx, cfg, exec, log)
|
|
41
|
+
registerAdmin(ctx, cfg, exec, log)
|
|
42
|
+
|
|
43
|
+
log('registered neuralos tools (list_instances, ask, graph, admin)')
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function registerList(ctx, cfg, log) {
|
|
47
|
+
ctx.registerTool({
|
|
48
|
+
name: 'neuralos_list_instances',
|
|
49
|
+
description:
|
|
50
|
+
'List the available neuralOS instances (on-device data agents). Each instance is a verified menu of probes over a real data source (a database, API, or file set). Use this first to see what can be queried.',
|
|
51
|
+
params: {},
|
|
52
|
+
handler: async () => {
|
|
53
|
+
const result = await listInstances(cfg)
|
|
54
|
+
log('list_instances')
|
|
55
|
+
return result
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function registerAsk(ctx, cfg, exec, log) {
|
|
61
|
+
ctx.registerTool({
|
|
62
|
+
name: 'neuralos_ask',
|
|
63
|
+
description:
|
|
64
|
+
'Ask a neuralOS instance a question in plain English. The on-device engine selects the probe, the instance bridge executes it against the real data source, and a verified digest comes back. Route data questions through this instead of querying the source directly.',
|
|
65
|
+
params: {
|
|
66
|
+
instance: { type: 'string', description: 'instance name from neuralos_list_instances' },
|
|
67
|
+
question: { type: 'string', description: 'the question in plain English, verbatim' },
|
|
68
|
+
},
|
|
69
|
+
handler: async (p) => {
|
|
70
|
+
const instance = String(p?.instance ?? '')
|
|
71
|
+
const question = String(p?.question ?? '')
|
|
72
|
+
const dir = await instanceDirFor(cfg, instance)
|
|
73
|
+
if (typeof dir !== 'string') return dir
|
|
74
|
+
const engine = await resolveEngine(cfg)
|
|
75
|
+
if (engine.error) return engine
|
|
76
|
+
const selection = await engineSelect(exec, engine, `${dir}/needle_menu.json`, question, { timeoutMs: cfg.timeoutMs })
|
|
77
|
+
if (selection.error) return { instance, question, ...selection }
|
|
78
|
+
const result = await executeProbe(exec, cfg, dir, selection.pick, selection.args, { timeoutMs: cfg.timeoutMs })
|
|
79
|
+
log(`ask ${instance} -> ${selection.pick} (conf ${selection.confidence})`)
|
|
80
|
+
return truncate({ instance, question, pick: selection.pick, confidence: selection.confidence, result })
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function registerGraph(ctx, cfg, exec, log) {
|
|
86
|
+
ctx.registerTool({
|
|
87
|
+
name: 'neuralos_graph',
|
|
88
|
+
description:
|
|
89
|
+
'Relationship questions over an instance: overview (the verified entity/edge map), neighbors (one-hop adjacency for a node fragment), or connect (path between two entities). Executes the graph probe directly.',
|
|
90
|
+
params: {
|
|
91
|
+
instance: { type: 'string', description: 'instance name' },
|
|
92
|
+
op: { type: 'string', description: 'overview | neighbors | connect' },
|
|
93
|
+
node: { type: 'string', description: 'node fragment for neighbors', optional: true },
|
|
94
|
+
a: { type: 'string', description: 'first entity for connect', optional: true },
|
|
95
|
+
b: { type: 'string', description: 'second entity for connect', optional: true },
|
|
96
|
+
},
|
|
97
|
+
handler: async (p) => {
|
|
98
|
+
const dir = await instanceDirFor(cfg, String(p?.instance ?? ''))
|
|
99
|
+
if (typeof dir !== 'string') return dir
|
|
100
|
+
const result = await graphProbe(exec, cfg, dir, { op: String(p?.op ?? 'overview'), node: p?.node, a: p?.a, b: p?.b })
|
|
101
|
+
log(`graph ${p?.op}`)
|
|
102
|
+
return truncate(result)
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function registerAdmin(ctx, cfg, exec, log) {
|
|
108
|
+
ctx.registerTool({
|
|
109
|
+
name: 'neuralos_admin',
|
|
110
|
+
description:
|
|
111
|
+
'Execute a WRITE/admin probe on an instance (boot a server, tag a resource, purge DNS). Destructive and reversible only per the instance design; the confirm parameter must be the literal string "yes" — that is the operator interlock. Read-only questions must use neuralos_ask instead.',
|
|
112
|
+
params: {
|
|
113
|
+
instance: { type: 'string', description: 'instance name' },
|
|
114
|
+
probe: { type: 'string', description: 'admin probe name, e.g. aws_power' },
|
|
115
|
+
args: { type: 'object', description: 'probe arguments as JSON', optional: true },
|
|
116
|
+
confirm: { type: 'string', description: 'must be the literal "yes"' },
|
|
117
|
+
},
|
|
118
|
+
handler: async (p) => {
|
|
119
|
+
if (p?.confirm !== 'yes') return { error: 'admin probes require confirm="yes" — restate what you intend to run and pass confirm="yes"' }
|
|
120
|
+
const dir = await instanceDirFor(cfg, String(p?.instance ?? ''))
|
|
121
|
+
if (typeof dir !== 'string') return dir
|
|
122
|
+
const result = await adminProbe(exec, cfg, dir, String(p?.probe ?? ''), p?.args ?? {})
|
|
123
|
+
log(`admin ${p?.probe} on ${p?.instance}`)
|
|
124
|
+
return truncate(result)
|
|
125
|
+
},
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function unregister() {
|
|
130
|
+
/* tools deregister with the plugin record; nothing to stop here */
|
|
131
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* neuralos — plugin extreme spec.
|
|
3
|
+
*
|
|
4
|
+
* Pure invariants, always run, no network and no engine required:
|
|
5
|
+
* config precedence, engine-name mapping, JSON recovery, instance
|
|
6
|
+
* discovery, engine selection against a FAKE engine script (node is the
|
|
7
|
+
* only dependency), probe execution against a FAKE python (the runner is
|
|
8
|
+
* exercised via an injected exec), the graph direct-execution rule, and
|
|
9
|
+
* the admin confirm interlock. The auto-download path is tested with an
|
|
10
|
+
* injected fetch that writes local files — CI never touches HuggingFace.
|
|
11
|
+
*
|
|
12
|
+
* Run: npx tsx plugins/neuralos/neuralos.extreme.spec.mts
|
|
13
|
+
*/
|
|
14
|
+
import { spawnSync } from 'node:child_process'
|
|
15
|
+
import { mkdtempSync, writeFileSync, mkdirSync, existsSync, readFileSync, chmodSync } from 'node:fs'
|
|
16
|
+
import { homedir, tmpdir } from 'node:os'
|
|
17
|
+
import { join } from 'node:path'
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
HF_BASE,
|
|
21
|
+
adminProbe,
|
|
22
|
+
bundledEngineName,
|
|
23
|
+
defaultExec,
|
|
24
|
+
engineSelect,
|
|
25
|
+
ensureEngine,
|
|
26
|
+
executeProbe,
|
|
27
|
+
graphProbe,
|
|
28
|
+
listInstances,
|
|
29
|
+
parseJsonObject,
|
|
30
|
+
resolveConfig,
|
|
31
|
+
resolveEngine,
|
|
32
|
+
truncate,
|
|
33
|
+
} from './neuralosEngine.mjs'
|
|
34
|
+
import { register } from './index.mjs'
|
|
35
|
+
|
|
36
|
+
let pass = 0
|
|
37
|
+
let fail = 0
|
|
38
|
+
const failures: string[] = []
|
|
39
|
+
|
|
40
|
+
function ok(cond: unknown, label: string, note = ''): void {
|
|
41
|
+
if (cond) {
|
|
42
|
+
pass++
|
|
43
|
+
console.log(`PASS ${label}`)
|
|
44
|
+
} else {
|
|
45
|
+
fail++
|
|
46
|
+
failures.push(`${label}${note ? ` — ${note}` : ''}`)
|
|
47
|
+
console.log(`FAIL ${label}${note ? ` — ${note}` : ''}`)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
// 1. Config precedence: settings block > env > default; blank = unset.
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
{
|
|
55
|
+
const cfg = resolveConfig({ getSettings: () => ({ neuralos: { pythonBin: '/framework/python3.12', instancesDir: ' ' } }) }, {})
|
|
56
|
+
ok(cfg.pythonBin === '/framework/python3.12', 'settings block wins for pythonBin')
|
|
57
|
+
ok(cfg.instancesDir === join(homedir(), 'neuralos-instances'), 'blank settings value falls through to default', cfg.instancesDir)
|
|
58
|
+
|
|
59
|
+
const cfgEnv = resolveConfig({ getSettings: () => ({}) }, { NEURALOS_INSTANCES_DIR: '/custom/instances' })
|
|
60
|
+
ok(cfgEnv.instancesDir === '/custom/instances', 'env wins when settings are silent')
|
|
61
|
+
|
|
62
|
+
const cfgOff = resolveConfig({ getSettings: () => ({ neuralos: { autoDownload: false } }) }, {})
|
|
63
|
+
ok(cfgOff.autoDownload === false, 'autoDownload can be disabled in settings')
|
|
64
|
+
const cfgOn = resolveConfig({ getSettings: () => ({}) }, {})
|
|
65
|
+
ok(cfgOn.autoDownload === true, 'autoDownload defaults on')
|
|
66
|
+
}
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// 2. Engine name mapping (macos-x64 has no published engine).
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
{
|
|
71
|
+
ok(bundledEngineName('darwin', 'arm64') === 'engine-macos-arm64', 'darwin/arm64 maps to engine-macos-arm64')
|
|
72
|
+
ok(bundledEngineName('linux', 'x64') === 'engine-linux-x86_64', 'linux/x64 maps to engine-linux-x86_64')
|
|
73
|
+
ok(bundledEngineName('linux', 'arm64') === 'engine-linux-arm64', 'linux/arm64 maps to engine-linux-arm64')
|
|
74
|
+
ok(bundledEngineName('win32', 'x64') === 'engine-windows-x86_64.exe', 'win32/x64 maps to engine-windows-x86_64.exe')
|
|
75
|
+
ok(bundledEngineName('darwin', 'x64') === null, 'darwin/x64 has no published engine')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// 3. JSON recovery from noisy engine output.
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
{
|
|
82
|
+
const parsed = parseJsonObject('noise {"function_calls":[{"name":"p"}], "confidence": 0.9} trailing')
|
|
83
|
+
ok(parsed?.function_calls?.[0]?.name === 'p', 'parseJsonObject recovers JSON embedded in noise')
|
|
84
|
+
ok(parseJsonObject('not json') === null, 'parseJsonObject returns null for garbage')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// 4. Instance discovery (fake instances root in a temp dir).
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
{
|
|
91
|
+
const root = mkdtempSync(join(tmpdir(), 'neuralos-spec-'))
|
|
92
|
+
mkdirSync(join(root, 'chinook'))
|
|
93
|
+
writeFileSync(join(root, 'chinook', 'needle_menu.json'), JSON.stringify([{ name: 'p1' }, { name: 'p2' }]))
|
|
94
|
+
mkdirSync(join(root, 'not-an-instance'))
|
|
95
|
+
|
|
96
|
+
const cfg = resolveConfig({}, { NEURALOS_INSTANCES_DIR: root })
|
|
97
|
+
const listed = await listInstances(cfg)
|
|
98
|
+
ok(
|
|
99
|
+
Array.isArray(listed) && listed.length === 1 && listed[0].name === 'chinook' && listed[0].probeCount === 2,
|
|
100
|
+
'listInstances finds only dirs with a needle_menu.json',
|
|
101
|
+
JSON.stringify(listed),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
const missing = await listInstances(cfg, { readdir: async () => { throw new Error('ENOENT') }, readFile: async () => '' })
|
|
105
|
+
ok(
|
|
106
|
+
(missing as { error?: string }).error?.includes('no instances directory'),
|
|
107
|
+
'listInstances reports a missing root as data',
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// 5. Engine selection against a FAKE engine (a node script).
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
{
|
|
115
|
+
const dir = mkdtempSync(join(tmpdir(), 'neuralos-engine-'))
|
|
116
|
+
const fakeEngine = join(dir, 'fake-engine.mjs')
|
|
117
|
+
writeFileSync(
|
|
118
|
+
fakeEngine,
|
|
119
|
+
`const args = process.argv.slice(2)
|
|
120
|
+
const qi = args.indexOf('--prompt')
|
|
121
|
+
const q = qi >= 0 ? args[qi + 1] : ''
|
|
122
|
+
if (q === 'refuse me') { console.log(JSON.stringify({ function_calls: [] })) }
|
|
123
|
+
else if (q === 'garbage') { console.log('not json at all') }
|
|
124
|
+
else if (q === 'crash') { process.stderr.write('boom'); process.exit(1) }
|
|
125
|
+
else { console.log(JSON.stringify({ function_calls: [{ name: 'transactions_count', arguments: { limit: 5 } }], confidence: 0.98 })) }
|
|
126
|
+
`,
|
|
127
|
+
)
|
|
128
|
+
const runEngine = (q: string) =>
|
|
129
|
+
engineSelect(
|
|
130
|
+
(cmd: string, args: string[]) => {
|
|
131
|
+
void cmd
|
|
132
|
+
const r = spawnSync(process.execPath, [fakeEngine, ...args.slice(2)], { encoding: 'utf8' })
|
|
133
|
+
return Promise.resolve({ ok: r.status === 0, code: r.status ?? 1, stdout: r.stdout ?? '', stderr: r.stderr ?? '' })
|
|
134
|
+
},
|
|
135
|
+
{ engineBin: fakeEngine, engineWeights: join(dir, 'weights.cact') },
|
|
136
|
+
'/i/needle_menu.json',
|
|
137
|
+
q,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
const hit = await runEngine('count invoices')
|
|
141
|
+
ok(hit.pick === 'transactions_count' && hit.confidence === 0.98, 'engineSelect parses the selection with confidence', JSON.stringify(hit))
|
|
142
|
+
const refused = await runEngine('refuse me')
|
|
143
|
+
ok((refused as { error?: string }).error === 'engine refused the question (no call selected)', 'refusal is data, not an exception')
|
|
144
|
+
const garbage = await runEngine('garbage')
|
|
145
|
+
ok((garbage as { error?: string }).error?.includes('engine output not JSON'), 'non-JSON engine output is data')
|
|
146
|
+
const crash = await runEngine('crash')
|
|
147
|
+
ok((crash as { error?: string }).error === 'engine exited 1: boom', 'engine crash with empty stdout is data')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// 6. Probe execution via an injected exec (the real path spawns python).
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
{
|
|
154
|
+
const cfg = resolveConfig({}, { NEURALOS_PYTHON: '/framework/python3.12' })
|
|
155
|
+
const seen: Array<{ cmd: string; args: string[]; env: Record<string, string> }> = []
|
|
156
|
+
const exec = (cmd: string, args: string[], opts: { env?: Record<string, string> }) => {
|
|
157
|
+
seen.push({ cmd, args, env: opts.env ?? {} })
|
|
158
|
+
return Promise.resolve({ ok: true, code: 0, stdout: '{"count": 42}', stderr: '' })
|
|
159
|
+
}
|
|
160
|
+
const result = await executeProbe(exec, cfg, '/i', 'transactions_count', { limit: 5 })
|
|
161
|
+
ok(result.count === 42, 'executeProbe returns the parsed digest')
|
|
162
|
+
ok(seen[0].cmd === '/framework/python3.12' && seen[0].args[0] === '-c', 'executeProbe spawns the configured python with the runner')
|
|
163
|
+
ok(
|
|
164
|
+
seen[0].env.NEURALOS_INSTANCE_DIR === '/i' && seen[0].env.NEURALOS_PROBE === 'transactions_count' && seen[0].env.NEURALOS_ARGS === '{"limit":5}',
|
|
165
|
+
'probe name and args travel through the environment, never the command line',
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
const failed = await executeProbe((() => Promise.resolve({ ok: false, code: 1, stdout: '', stderr: 'boom' })) as never, cfg, '/i', 'p', {})
|
|
169
|
+
ok((failed as { error?: string }).error === 'probe exited 1: boom', 'probe failure surfaces as data')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// 7. Graph direct execution + admin interlock.
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
{
|
|
176
|
+
const dir = mkdtempSync(join(tmpdir(), 'neuralos-graph-'))
|
|
177
|
+
writeFileSync(
|
|
178
|
+
join(dir, 'needle_menu.json'),
|
|
179
|
+
JSON.stringify([{ name: 'cb_graph_overview' }, { name: 'cb_graph_neighbors' }, { name: 'transactions_count' }]),
|
|
180
|
+
)
|
|
181
|
+
const cfg = resolveConfig({}, {})
|
|
182
|
+
const exec = () => Promise.resolve({ ok: true, code: 0, stdout: '{"nodes":[]}', stderr: '' })
|
|
183
|
+
|
|
184
|
+
const overview = await graphProbe(exec, cfg, dir, { op: 'overview' })
|
|
185
|
+
ok((overview as { probe?: string }).probe === 'cb_graph_overview' && (overview as { nodes?: unknown[] }).nodes?.length === 0, 'graph overview executes the probe directly by name')
|
|
186
|
+
const neighbors = await graphProbe(exec, cfg, dir, { op: 'neighbors', node: '4001234567' })
|
|
187
|
+
ok((neighbors as { probe?: string }).probe === 'cb_graph_neighbors', 'graph neighbors executes by name (numeric fragments bypass engine grounding)')
|
|
188
|
+
const missingOp = await graphProbe(exec, cfg, dir, { op: 'connect', a: 'x', b: 'y' })
|
|
189
|
+
ok(
|
|
190
|
+
(missingOp as { error?: string }).error === 'this instance has no connect graph probe' &&
|
|
191
|
+
Array.isArray((missingOp as { available_graph_probes?: string[] }).available_graph_probes),
|
|
192
|
+
'a missing graph op lists what exists instead of guessing',
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
const admin = await adminProbe(exec, cfg, dir, 'aws_tag', { confirm: 'yes' })
|
|
196
|
+
ok(admin.admin === true && admin.probe === 'aws_tag', 'admin wraps the result with the probe name and admin flag')
|
|
197
|
+
const badName = await adminProbe(exec, cfg, dir, 'rm -rf', {})
|
|
198
|
+
ok((badName as { error?: string }).error === 'invalid probe name: rm -rf', 'admin rejects probe names that are not identifiers')
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
// 8. Engine resolution: settings override wins; auto-download provisions the
|
|
203
|
+
// cache via an injected fetch (no network in CI); idempotent on re-run.
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
{
|
|
206
|
+
const explicitDir = mkdtempSync(join(tmpdir(), 'neuralos-explicit-'))
|
|
207
|
+
writeFileSync(join(explicitDir, 'needle'), 'fake-engine')
|
|
208
|
+
writeFileSync(join(explicitDir, 'needle3.cact'), 'fake-weights')
|
|
209
|
+
const cacheAlt = mkdtempSync(join(tmpdir(), 'neuralos-cache-alt-'))
|
|
210
|
+
writeFileSync(join(cacheAlt, 'engine-macos-arm64'), 'cache-engine')
|
|
211
|
+
writeFileSync(join(cacheAlt, 'needle3.cact'), 'cache-weights')
|
|
212
|
+
const explicit = await resolveEngine(
|
|
213
|
+
resolveConfig({ getSettings: () => ({ neuralos: { engineBin: join(explicitDir, 'needle'), engineWeights: join(explicitDir, 'needle3.cact'), cacheDir: cacheAlt } }) }, {}),
|
|
214
|
+
{ platform: 'darwin', arch: 'arm64' },
|
|
215
|
+
)
|
|
216
|
+
ok(explicit.engineBin === join(explicitDir, 'needle'), 'explicit settings engine wins over a populated cache')
|
|
217
|
+
|
|
218
|
+
const cache = mkdtempSync(join(tmpdir(), 'neuralos-cache-'))
|
|
219
|
+
const downloads: string[] = []
|
|
220
|
+
const fetchImpl = async (url: string, target: string) => {
|
|
221
|
+
downloads.push(url)
|
|
222
|
+
writeFileSync(target, `fake:${url}`)
|
|
223
|
+
}
|
|
224
|
+
const cfg = resolveConfig({ getSettings: () => ({}) }, { NEURALOS_INSTANCES_DIR: mkdtempSync(join(tmpdir(), 'neuralos-none-')), NEURALOS_CACHE_DIR: cache })
|
|
225
|
+
const first = await ensureEngine({ platform: 'darwin', arch: 'arm64', cacheDir: cache, fetchImpl })
|
|
226
|
+
ok(first.engineBin === join(cache, 'engine-macos-arm64') && first.engineWeights === join(cache, 'needle3.cact'), 'ensureEngine provisions weights + per-arch engine into the cache', JSON.stringify(first))
|
|
227
|
+
ok(downloads.length === 2 && downloads[0] === `${HF_BASE}/needle3.cact` && downloads[1].includes('macos-arm64/needle'), 'downloads come from the public Cactus-Compute/needle3 URLs')
|
|
228
|
+
ok(existsSync(join(cache, 'engine-macos-arm64')), 'the engine file exists on disk after provisioning')
|
|
229
|
+
|
|
230
|
+
const second = await ensureEngine({ platform: 'darwin', arch: 'arm64', cacheDir: cache, fetchImpl })
|
|
231
|
+
ok(second.engineBin === first.engineBin && downloads.length === 2, 'provisioning is idempotent — no re-download when cached')
|
|
232
|
+
|
|
233
|
+
// resolution now finds the cache without any download
|
|
234
|
+
const resolved = await resolveEngine(cfg, { platform: 'darwin', arch: 'arm64' })
|
|
235
|
+
ok(resolved.engineBin === join(cache, 'engine-macos-arm64'), 'resolveEngine picks the provisioned cache copy')
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
// 9. Tool registration + the admin confirm interlock at the tool layer.
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
{
|
|
242
|
+
const registered: Array<{ name: string; params: Record<string, unknown>; handler: (args: unknown) => Promise<unknown> }> = []
|
|
243
|
+
const logged: string[] = []
|
|
244
|
+
register({
|
|
245
|
+
registerTool: (t) => registered.push(t as never),
|
|
246
|
+
log: (l: string) => logged.push(l),
|
|
247
|
+
getSettings: () => ({}),
|
|
248
|
+
})
|
|
249
|
+
ok(
|
|
250
|
+
registered.map((t) => t.name).sort().join(',') === 'neuralos_admin,neuralos_ask,neuralos_graph,neuralos_list_instances',
|
|
251
|
+
'register() wires exactly the four neuralos tools',
|
|
252
|
+
)
|
|
253
|
+
const admin = registered.find((t) => t.name === 'neuralos_admin')
|
|
254
|
+
const denied = (await admin.handler({ instance: 'aws', probe: 'aws_power', confirm: 'no' })) as { error?: string }
|
|
255
|
+
ok(denied.error?.includes('confirm="yes"'), 'the tool layer refuses admin without confirm="yes"')
|
|
256
|
+
const unknown = registered.find((t) => t.name === 'neuralos_ask')
|
|
257
|
+
const answered = (await unknown.handler({ instance: 'does-not-exist', question: 'x' })) as { error?: string }
|
|
258
|
+
ok(answered.error?.includes("no instance named 'does-not-exist'"), 'ask answers an unknown instance as data')
|
|
259
|
+
ok(logged.some((l) => l.includes('registered neuralos tools')), 'the plugin announces registration through ctx.log')
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
// 10. Digest truncation keeps tool results bounded.
|
|
264
|
+
// ---------------------------------------------------------------------------
|
|
265
|
+
{
|
|
266
|
+
const big = { rows: 'x'.repeat(20_000) }
|
|
267
|
+
const text = truncate(big, 6000)
|
|
268
|
+
ok(text.length < 21_000 && text.includes('truncated at 6000 chars'), 'truncate caps large digests with an honest marker')
|
|
269
|
+
ok(truncate('small') === 'small', 'small digests pass through untouched')
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
// 11. The REAL defaultExec forwards opts.env (probe transport) to the child
|
|
274
|
+
// while keeping the parent environment (PATH etc.) intact.
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
{
|
|
277
|
+
const exec = defaultExec()
|
|
278
|
+
const out = await exec(process.execPath, ['-e', 'console.log(process.env.NEURALOS_SPEC_PROBE ?? "missing")'], { env: { NEURALOS_SPEC_PROBE: 'forwarded' } })
|
|
279
|
+
ok(out.code === 0 && out.stdout.trim() === 'forwarded', 'defaultExec forwards opts.env to the child process')
|
|
280
|
+
const plain = await exec(process.execPath, ['-e', 'console.log(Boolean(process.env.PATH))'])
|
|
281
|
+
ok(plain.code === 0 && plain.stdout.trim() === 'true', 'defaultExec keeps the parent environment for plain calls')
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
console.log(`\n${pass} passed, ${fail} failed`)
|
|
285
|
+
if (fail > 0) {
|
|
286
|
+
console.log(`FAILURES:\n${failures.map((f) => ` - ${f}`).join('\n')}`)
|
|
287
|
+
process.exit(1)
|
|
288
|
+
}
|
|
289
|
+
console.log('neuralos: ALL TESTS PASSED')
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* neuralosEngine — the neuralOS runtime for the RTerm neuralos plugin.
|
|
3
|
+
*
|
|
4
|
+
* neuralOS instances are on-device data agents: each instance directory
|
|
5
|
+
* carries a probe menu (needle_menu.json) over a real data source, the
|
|
6
|
+
* 121M engine binary picks the probe that answers a question, and the
|
|
7
|
+
* instance's bridge (Python) executes it against the source. Everything
|
|
8
|
+
* here returns errors as data — a missing engine or instance is an
|
|
9
|
+
* answer, never a thrown exception.
|
|
10
|
+
*
|
|
11
|
+
* Engine resolution order (first existing wins):
|
|
12
|
+
* 1. settings.neuralos.engineBin/engineWeights (or NEURALOS_ENGINE_* env)
|
|
13
|
+
* 2. the desktop bundle: {process.resourcesPath}/neuralos/<per-arch engine>
|
|
14
|
+
* (electron-builder extraResources; present in the packaged app only)
|
|
15
|
+
* 3. the shared cache ~/.cache/neuralos/ (auto-downloaded on first use —
|
|
16
|
+
* this is what makes `npm i -g neuralos` self-provisioning: one fetch,
|
|
17
|
+
* then offline forever)
|
|
18
|
+
* 4. <instancesRoot>/engine/needle (+ needle3.cact) — the fleet convention
|
|
19
|
+
* If nothing exists and autoDownload is enabled, the cache is provisioned
|
|
20
|
+
* from https://huggingface.co/Cactus-Compute/needle3 (public, no auth).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { execFile as cpExecFile, spawn } from 'node:child_process'
|
|
24
|
+
import { promises as fs, constants as fsConstants } from 'node:fs'
|
|
25
|
+
import { homedir } from 'node:os'
|
|
26
|
+
import path from 'node:path'
|
|
27
|
+
|
|
28
|
+
export const HF_BASE = 'https://huggingface.co/Cactus-Compute/needle3/resolve/main'
|
|
29
|
+
export const WEIGHTS_NAME = 'needle3.cact'
|
|
30
|
+
export const MAX_RESULT_CHARS = 6000
|
|
31
|
+
|
|
32
|
+
/** Per-arch bundled engine name, matching the release-workflow download. */
|
|
33
|
+
export function bundledEngineName(platform, arch) {
|
|
34
|
+
const os = platform === 'darwin' ? 'macos' : platform === 'win32' ? 'windows' : 'linux'
|
|
35
|
+
if (os === 'macos') return arch === 'arm64' ? 'engine-macos-arm64' : null
|
|
36
|
+
if (os === 'linux') return arch === 'arm64' ? 'engine-linux-arm64' : arch === 'x64' ? 'engine-linux-x86_64' : null
|
|
37
|
+
return arch === 'arm64' ? 'engine-windows-arm64.exe' : arch === 'x64' ? 'engine-windows-x86_64.exe' : null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The HF platform folder for a bundled engine name. */
|
|
41
|
+
export function hfFolderFor(platform, arch) {
|
|
42
|
+
const os = platform === 'darwin' ? 'macos' : platform === 'win32' ? 'windows' : 'linux'
|
|
43
|
+
if (os === 'macos') return 'macos-arm64'
|
|
44
|
+
if (os === 'linux') return arch === 'arm64' ? 'linux-arm64' : 'linux-x86_64'
|
|
45
|
+
return arch === 'arm64' ? 'windows-arm64' : 'windows-x86_64'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function cacheDir(env = process.env) {
|
|
49
|
+
if (env.NEURALOS_CACHE_DIR) return env.NEURALOS_CACHE_DIR
|
|
50
|
+
return path.join(homedir(), '.cache', 'neuralos')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Settings block -> env -> default. Blank values mean unset. */
|
|
54
|
+
function pick(settingsValue, envValue, fallback) {
|
|
55
|
+
const s = typeof settingsValue === 'string' ? settingsValue.trim() : ''
|
|
56
|
+
if (s) return s
|
|
57
|
+
const e = typeof envValue === 'string' ? envValue.trim() : ''
|
|
58
|
+
if (e) return e
|
|
59
|
+
return fallback
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function resolveConfig(ctx = {}, env = process.env) {
|
|
63
|
+
const settings = (typeof ctx.getSettings === 'function' ? ctx.getSettings() : ctx.settings) || {}
|
|
64
|
+
const block = settings.neuralos || {}
|
|
65
|
+
return {
|
|
66
|
+
instancesDir: pick(block.instancesDir, env.NEURALOS_INSTANCES_DIR, path.join(homedir(), 'neuralos-instances')),
|
|
67
|
+
pythonBin: pick(block.pythonBin, env.NEURALOS_PYTHON, 'python3'),
|
|
68
|
+
engineBin: pick(block.engineBin, env.NEURALOS_ENGINE_BIN) || null,
|
|
69
|
+
engineWeights: pick(block.engineWeights, env.NEURALOS_ENGINE_WEIGHTS) || null,
|
|
70
|
+
autoDownload: block.autoDownload !== false && env.NEURALOS_AUTO_DOWNLOAD !== '0',
|
|
71
|
+
cacheDir: pick(block.cacheDir, env.NEURALOS_CACHE_DIR, cacheDir(env)),
|
|
72
|
+
timeoutMs: Number(block.timeoutMs) > 0 ? Number(block.timeoutMs) : 120000,
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Default execFile promisified; injectable for the spec. */
|
|
77
|
+
export function defaultExec() {
|
|
78
|
+
return (cmd, args, opts = {}) =>
|
|
79
|
+
new Promise((resolve) => {
|
|
80
|
+
const execEnv = opts.env ? { ...process.env, ...opts.env } : process.env
|
|
81
|
+
const child = cpExecFile(cmd, args, { timeout: opts.timeoutMs ?? 120000, maxBuffer: 16 * 1024 * 1024, env: execEnv }, (err, stdout, stderr) => {
|
|
82
|
+
resolve({ ok: !err, code: err ? err.code ?? 1 : 0, stdout: String(stdout ?? ''), stderr: String(stderr ?? '') })
|
|
83
|
+
})
|
|
84
|
+
void child
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function exists(p) {
|
|
89
|
+
try {
|
|
90
|
+
await fs.access(p, fsConstants.F_OK)
|
|
91
|
+
return true
|
|
92
|
+
} catch {
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Download one file over HTTPS into dir with its basename (idempotent). */
|
|
98
|
+
async function download(deps, url, dir) {
|
|
99
|
+
await fs.mkdir(dir, { recursive: true })
|
|
100
|
+
const target = path.join(dir, path.basename(url))
|
|
101
|
+
if (await exists(target)) return target
|
|
102
|
+
const tmp = `${target}.part`
|
|
103
|
+
if (!deps.fetchImpl) {
|
|
104
|
+
const res = await fetch(url, { redirect: 'follow' })
|
|
105
|
+
if (!res.ok) throw new Error(`download ${url} -> HTTP ${res.status}`)
|
|
106
|
+
const buf = Buffer.from(await res.arrayBuffer())
|
|
107
|
+
await fs.writeFile(tmp, buf)
|
|
108
|
+
} else {
|
|
109
|
+
await deps.fetchImpl(url, tmp)
|
|
110
|
+
}
|
|
111
|
+
await fs.rename(tmp, target)
|
|
112
|
+
return target
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Provision the shared cache with the weights + this platform's engine.
|
|
117
|
+
* One-time (~36 MB); afterwards everything resolves offline. Injectable
|
|
118
|
+
* fetch/download for the spec; the real path uses global fetch (Node >=18).
|
|
119
|
+
*/
|
|
120
|
+
export async function ensureEngine(deps, { force = false } = {}) {
|
|
121
|
+
const engineName = bundledEngineName(deps.platform ?? process.platform, deps.arch ?? process.arch)
|
|
122
|
+
if (!engineName) return { error: `no published neuralOS engine for ${deps.platform ?? process.platform}-${deps.arch ?? process.arch}` }
|
|
123
|
+
const dir = deps.cacheDir ?? cacheDir()
|
|
124
|
+
const weightsPath = path.join(dir, WEIGHTS_NAME)
|
|
125
|
+
const enginePath = path.join(dir, engineName)
|
|
126
|
+
try {
|
|
127
|
+
if (force || !(await exists(weightsPath))) {
|
|
128
|
+
await download(deps, `${HF_BASE}/${WEIGHTS_NAME}`, dir)
|
|
129
|
+
}
|
|
130
|
+
if (force || !(await exists(enginePath))) {
|
|
131
|
+
await download(deps, `${HF_BASE}/${hfFolderFor(deps.platform ?? process.platform, deps.arch ?? process.arch)}/${engineName.startsWith('engine-windows') ? 'needle.exe' : 'needle'}`, dir)
|
|
132
|
+
// HF keeps upstream names; rename to the per-arch bundled name.
|
|
133
|
+
const raw = engineName.startsWith('engine-windows') ? 'needle.exe' : 'needle'
|
|
134
|
+
const rawPath = path.join(dir, raw)
|
|
135
|
+
if (rawPath !== enginePath && (await exists(rawPath))) await fs.rename(rawPath, enginePath)
|
|
136
|
+
}
|
|
137
|
+
await fs.chmod(enginePath, 0o755).catch(() => {})
|
|
138
|
+
return { engineBin: enginePath, engineWeights: weightsPath }
|
|
139
|
+
} catch (e) {
|
|
140
|
+
return { error: `engine auto-download failed: ${String(e?.message ?? e)}` }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function resolveEngine(cfg, deps = {}) {
|
|
145
|
+
const platform = deps.platform ?? process.platform
|
|
146
|
+
const arch = deps.arch ?? process.arch
|
|
147
|
+
const candidates = []
|
|
148
|
+
if (cfg.engineBin && cfg.engineWeights) candidates.push({ bin: cfg.engineBin, weights: cfg.engineWeights })
|
|
149
|
+
const resources = (deps.resourcesPath ?? process.resourcesPath) ?? undefined
|
|
150
|
+
const bundled = bundledEngineName(platform, arch)
|
|
151
|
+
if (resources && bundled) {
|
|
152
|
+
candidates.push({ bin: path.join(resources, 'neuralos', bundled), weights: path.join(resources, 'neuralos', WEIGHTS_NAME) })
|
|
153
|
+
}
|
|
154
|
+
if (bundled) {
|
|
155
|
+
candidates.push({ bin: path.join(cfg.cacheDir, bundled), weights: path.join(cfg.cacheDir, WEIGHTS_NAME) })
|
|
156
|
+
}
|
|
157
|
+
candidates.push({ bin: path.join(cfg.instancesDir, 'engine', 'needle'), weights: path.join(cfg.instancesDir, 'engine', WEIGHTS_NAME) })
|
|
158
|
+
for (const c of candidates) {
|
|
159
|
+
if ((await exists(c.bin)) && (await exists(c.weights))) return { engineBin: c.bin, engineWeights: c.weights }
|
|
160
|
+
}
|
|
161
|
+
if (cfg.autoDownload) {
|
|
162
|
+
return ensureEngine({ ...deps, cacheDir: cfg.cacheDir, platform, arch })
|
|
163
|
+
}
|
|
164
|
+
return { error: 'neuralOS engine not found — set neuralos.engineBin/engineWeights (settings), install the desktop bundle, or enable autoDownload' }
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function parseJsonObject(text) {
|
|
168
|
+
try {
|
|
169
|
+
return JSON.parse(text)
|
|
170
|
+
} catch {
|
|
171
|
+
const first = text.indexOf('{')
|
|
172
|
+
const last = text.lastIndexOf('}')
|
|
173
|
+
if (first === -1 || last <= first) return null
|
|
174
|
+
try {
|
|
175
|
+
return JSON.parse(text.slice(first, last + 1))
|
|
176
|
+
} catch {
|
|
177
|
+
return null
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export async function listInstances(cfg, deps = {}) {
|
|
183
|
+
const readdir = deps.readdir ?? fs.readdir
|
|
184
|
+
const readFile = deps.readFile ?? fs.readFile
|
|
185
|
+
let entries
|
|
186
|
+
try {
|
|
187
|
+
entries = await readdir(cfg.instancesDir, { withFileTypes: true })
|
|
188
|
+
} catch {
|
|
189
|
+
return { error: `no instances directory at ${cfg.instancesDir} — set neuralos.instancesDir (settings) or NEURALOS_INSTANCES_DIR` }
|
|
190
|
+
}
|
|
191
|
+
const instances = []
|
|
192
|
+
for (const entry of entries) {
|
|
193
|
+
if (!entry.isDirectory()) continue
|
|
194
|
+
const dir = path.join(cfg.instancesDir, entry.name)
|
|
195
|
+
try {
|
|
196
|
+
const raw = await readFile(path.join(dir, 'needle_menu.json'), 'utf-8')
|
|
197
|
+
const menu = JSON.parse(raw)
|
|
198
|
+
if (!Array.isArray(menu)) continue
|
|
199
|
+
instances.push({ name: entry.name, probeCount: menu.length, probes: menu.slice(0, 40).map((t) => t.name) })
|
|
200
|
+
} catch {
|
|
201
|
+
continue
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return instances
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export async function instanceDirFor(cfg, instance) {
|
|
208
|
+
if (!/^[A-Za-z0-9._-]+$/.test(instance)) return { error: `invalid instance name: ${instance}` }
|
|
209
|
+
const dir = path.join(cfg.instancesDir, instance)
|
|
210
|
+
if (!(await exists(path.join(dir, 'needle_menu.json')))) {
|
|
211
|
+
return { error: `no instance named '${instance}' (see neuralos_list_instances)` }
|
|
212
|
+
}
|
|
213
|
+
return dir
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export async function engineSelect(exec, engine, menuPath, question, opts = {}) {
|
|
217
|
+
let out
|
|
218
|
+
try {
|
|
219
|
+
out = await exec(engine.engineBin, ['--model', engine.engineWeights, '--tools', menuPath, '--prompt', question], { timeoutMs: opts.timeoutMs })
|
|
220
|
+
} catch (e) {
|
|
221
|
+
return { error: `engine execution failed: ${String(e?.message ?? e)}` }
|
|
222
|
+
}
|
|
223
|
+
if (out.code !== 0 && !out.stdout.trim()) return { error: `engine exited ${out.code}: ${out.stderr.slice(0, 200)}` }
|
|
224
|
+
const parsed = parseJsonObject(out.stdout)
|
|
225
|
+
if (!parsed) return { error: `engine output not JSON: ${out.stdout.slice(0, 120)}` }
|
|
226
|
+
const calls = parsed.function_calls
|
|
227
|
+
if (!Array.isArray(calls) || calls.length === 0 || !calls[0]?.name) {
|
|
228
|
+
return { error: 'engine refused the question (no call selected)' }
|
|
229
|
+
}
|
|
230
|
+
return { pick: calls[0].name, args: calls[0].arguments ?? {}, confidence: typeof parsed.confidence === 'number' ? parsed.confidence : null }
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const PROBE_RUNNER = [
|
|
234
|
+
'import json, os, sys',
|
|
235
|
+
'sys.path.insert(0, os.environ["NEURALOS_INSTANCE_DIR"])',
|
|
236
|
+
'import bridge',
|
|
237
|
+
'fn = getattr(bridge, os.environ["NEURALOS_PROBE"], None)',
|
|
238
|
+
'if fn is None:',
|
|
239
|
+
' print(json.dumps({"error": "unknown probe", "probe": os.environ["NEURALOS_PROBE"]}))',
|
|
240
|
+
'else:',
|
|
241
|
+
' print(json.dumps(fn(**json.loads(os.environ.get("NEURALOS_ARGS", "{}"))), default=str))',
|
|
242
|
+
].join('\n')
|
|
243
|
+
|
|
244
|
+
export async function executeProbe(exec, cfg, instanceDir, probe, args, opts = {}) {
|
|
245
|
+
const env = {
|
|
246
|
+
NEURALOS_INSTANCE_DIR: instanceDir,
|
|
247
|
+
NEURALOS_PROBE: probe,
|
|
248
|
+
NEURALOS_ARGS: JSON.stringify(args ?? {}),
|
|
249
|
+
}
|
|
250
|
+
let out
|
|
251
|
+
try {
|
|
252
|
+
out = await exec(cfg.pythonBin, ['-c', PROBE_RUNNER], { ...opts, env })
|
|
253
|
+
} catch (e) {
|
|
254
|
+
return { error: `probe execution failed: ${String(e?.message ?? e)}` }
|
|
255
|
+
}
|
|
256
|
+
if (out.code !== 0 && !out.stdout.trim()) return { error: `probe exited ${out.code}: ${out.stderr.slice(0, 200)}` }
|
|
257
|
+
const parsed = parseJsonObject(out.stdout)
|
|
258
|
+
if (!parsed || typeof parsed !== 'object') return { error: `probe output not JSON: ${out.stdout.slice(0, 120)}` }
|
|
259
|
+
return parsed
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Graph probes execute DIRECTLY by name (bypassing engine selection): probe
|
|
264
|
+
* names follow the instance convention <prefix>_graph_<op>, and the 121M
|
|
265
|
+
* model grounds numeric fragments poorly — direct execution is the fix.
|
|
266
|
+
*/
|
|
267
|
+
export async function graphProbe(exec, cfg, instanceDir, input) {
|
|
268
|
+
if (!['overview', 'neighbors', 'connect'].includes(input.op)) return { error: `unknown graph op: ${input.op}` }
|
|
269
|
+
const raw = await fs.readFile(path.join(instanceDir, 'needle_menu.json'), 'utf-8')
|
|
270
|
+
const menu = JSON.parse(raw)
|
|
271
|
+
const graphNames = menu.map((t) => t.name).filter((n) => n.includes('_graph_'))
|
|
272
|
+
const probe = graphNames.find((n) => n.endsWith(`_graph_${input.op}`))
|
|
273
|
+
if (!probe) return { error: `this instance has no ${input.op} graph probe`, available_graph_probes: graphNames }
|
|
274
|
+
const args = input.op === 'neighbors' ? { node: input.node ?? '' } : input.op === 'connect' ? { a: input.a ?? '', b: input.b ?? '' } : {}
|
|
275
|
+
const result = await executeProbe(exec, cfg, instanceDir, probe, args)
|
|
276
|
+
return { probe, ...result }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Admin probes are write-capable bridge functions; the confirm='yes' literal
|
|
281
|
+
* is the operator interlock (enforced at the tool schema) and this guard
|
|
282
|
+
* keeps probe names to plain identifiers.
|
|
283
|
+
*/
|
|
284
|
+
export async function adminProbe(exec, cfg, instanceDir, probe, args) {
|
|
285
|
+
if (!/^[A-Za-z0-9_]+$/.test(probe)) return { error: `invalid probe name: ${probe}` }
|
|
286
|
+
const result = await executeProbe(exec, cfg, instanceDir, probe, args ?? {})
|
|
287
|
+
return { probe, admin: true, result }
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function truncate(value, max = MAX_RESULT_CHARS) {
|
|
291
|
+
const text = typeof value === 'string' ? value : JSON.stringify(value)
|
|
292
|
+
if (text.length <= max) return text
|
|
293
|
+
return `${text.slice(0, max)}\n… (truncated at ${max} chars)`
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Streamed engine invocation for long-running selections (unused by default). */
|
|
297
|
+
export function engineSelectStreamed(engine, menuPath, question) {
|
|
298
|
+
return spawn(engine.engineBin, ['--model', engine.engineWeights, '--tools', menuPath, '--prompt', question], { stdio: ['ignore', 'pipe', 'pipe'] })
|
|
299
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "neuralos",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "neuralOS instances as RTerm agent tools — on-device data agents over verified probe menus (chinook, cyberbank, aws, efraud, ...). The 121M neuralOS engine selects the probe for a plain-English question and the instance's Python bridge executes it against the real data source; the engine ships with the desktop bundle and the standalone backend auto-provisions ~/.cache/neuralos on first use. Four tools: neuralos_list_instances, neuralos_ask, neuralos_graph (relationship maps), neuralos_admin (write probes behind a confirm=\"yes\" interlock). Config in Settings (neuralos block: instancesDir, pythonBin, engineBin/engineWeights, autoDownload); instances root defaults to ~/neuralos-instances.",
|
|
5
|
+
"entry": "index.mjs",
|
|
6
|
+
"tools": [
|
|
7
|
+
"neuralos_list_instances",
|
|
8
|
+
"neuralos_ask",
|
|
9
|
+
"neuralos_graph",
|
|
10
|
+
"neuralos_admin"
|
|
11
|
+
],
|
|
12
|
+
"permissions": [
|
|
13
|
+
"exec"
|
|
14
|
+
]
|
|
15
|
+
}
|