smart-home-engine 1.52.0 → 1.53.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/dist/web/assets/index-COKTfw7g.js +298 -0
- package/dist/web/assets/index-x3Sn9IKo.css +1 -0
- package/dist/web/assets/{tsMode-CQwwRC_t.js → tsMode-BIGdBOAo.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/src/web/ai-api.js +33 -16
- package/src/web/ai-context.js +4 -3
- package/src/web/ai-sandbox-worker.js +84 -0
- package/src/web/ai-sandbox.js +63 -0
- package/src/web/ai-tools.js +610 -60
- package/src/web/prompts/compact.md +3 -3
- package/src/web/prompts/formats-notools.md +14 -0
- package/src/web/prompts/formats.md +2 -13
- package/src/web/prompts/tools.md +12 -4
- package/src/web/server.js +3 -1
- package/dist/web/assets/index-BswjzIm1.css +0 -1
- package/dist/web/assets/index-CD6lY3HK.js +0 -296
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{m as O}from"./monaco-langs-DKJI_o9J.js";import{t as I}from"./index-
|
|
1
|
+
import{m as O}from"./monaco-langs-DKJI_o9J.js";import{t as I}from"./index-COKTfw7g.js";/*!-----------------------------------------------------------------------------
|
|
2
2
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
3
3
|
* Version: 0.52.2(404545bded1df6ffa41ea0af4e8ddb219018c6c1)
|
|
4
4
|
* Released under the MIT license
|
package/dist/web/index.html
CHANGED
|
@@ -175,10 +175,10 @@
|
|
|
175
175
|
}
|
|
176
176
|
})();
|
|
177
177
|
</script>
|
|
178
|
-
<script type="module" crossorigin src="/assets/index-
|
|
178
|
+
<script type="module" crossorigin src="/assets/index-COKTfw7g.js"></script>
|
|
179
179
|
<link rel="modulepreload" crossorigin href="/assets/monaco-langs-DKJI_o9J.js">
|
|
180
180
|
<link rel="stylesheet" crossorigin href="/assets/monaco-langs-DyX1CsEw.css">
|
|
181
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
181
|
+
<link rel="stylesheet" crossorigin href="/assets/index-x3Sn9IKo.css">
|
|
182
182
|
</head>
|
|
183
183
|
<body>
|
|
184
184
|
<div id="app"></div>
|
package/package.json
CHANGED
package/src/web/ai-api.js
CHANGED
|
@@ -349,6 +349,23 @@ function answerText(content) {
|
|
|
349
349
|
* @returns {Promise<{ message: string, usage?: object }>}
|
|
350
350
|
*/
|
|
351
351
|
/** run a tool; a result of { text, event } sends the event to the chat and hands the text to the model */
|
|
352
|
+
/**
|
|
353
|
+
* The tool calls of one round, executed concurrently (roadmap I31): the model is invited to ask for every
|
|
354
|
+
* independent tool in the same turn, and a slow history query must not delay a topic lookup. The call events go
|
|
355
|
+
* out first, in the model's order; each result event goes out as its tool finishes; the contents come back in
|
|
356
|
+
* the model's order, so the tool_result blocks keep their ids.
|
|
357
|
+
*/
|
|
358
|
+
async function runToolCalls(calls, ctx, onEvent) {
|
|
359
|
+
for (const c of calls) onEvent?.({ type: 'tool_call', name: c.name, args: c.args });
|
|
360
|
+
return Promise.all(
|
|
361
|
+
calls.map(async (c) => {
|
|
362
|
+
const content = await runToolForChat(c.name, c.args, ctx, onEvent);
|
|
363
|
+
onEvent?.({ type: 'tool_result', name: c.name, content });
|
|
364
|
+
return content;
|
|
365
|
+
}),
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
352
369
|
async function runToolForChat(name, args, ctx, onEvent) {
|
|
353
370
|
const out = await executeTool(name, args, ctx);
|
|
354
371
|
if (out && typeof out === 'object') {
|
|
@@ -420,14 +437,12 @@ async function resolveAndGetAnswer(ai, messages, toolContext, onEvent) {
|
|
|
420
437
|
if (isAnthropic) {
|
|
421
438
|
// the assistant turn goes back unchanged: the current models bind their thinking blocks to the turn
|
|
422
439
|
msgs = [...msgs, { role: 'assistant', content: result.assistantMsg }];
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
onEvent
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
toolResultBlocks.push({ type: 'tool_result', tool_use_id: tc.id, content });
|
|
430
|
-
}
|
|
440
|
+
const contents = await runToolCalls(
|
|
441
|
+
result.toolCalls.map((tc) => ({ name: tc.name, args: tc.input || {} })),
|
|
442
|
+
toolContext,
|
|
443
|
+
onEvent,
|
|
444
|
+
);
|
|
445
|
+
const toolResultBlocks = result.toolCalls.map((tc, i) => ({ type: 'tool_result', tool_use_id: tc.id, content: contents[i] }));
|
|
431
446
|
msgs = [...msgs, { role: 'user', content: toolResultBlocks }];
|
|
432
447
|
} else {
|
|
433
448
|
// Strip any draft content alongside tool_calls — if kept, the model reproduces
|
|
@@ -435,19 +450,17 @@ async function resolveAndGetAnswer(ai, messages, toolContext, onEvent) {
|
|
|
435
450
|
const assistantEntry = { ...result.assistantMsg, role: 'assistant' };
|
|
436
451
|
if (assistantEntry.content && assistantEntry.tool_calls?.length) assistantEntry.content = null;
|
|
437
452
|
msgs = [...msgs, assistantEntry];
|
|
438
|
-
|
|
439
|
-
const name = tc.function.name;
|
|
453
|
+
const calls = result.toolCalls.map((tc) => {
|
|
440
454
|
let args;
|
|
441
455
|
try {
|
|
442
456
|
args = JSON.parse(tc.function.arguments || '{}');
|
|
443
457
|
} catch {
|
|
444
458
|
args = {};
|
|
445
459
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
}
|
|
460
|
+
return { name: tc.function.name, args };
|
|
461
|
+
});
|
|
462
|
+
const contents = await runToolCalls(calls, toolContext, onEvent);
|
|
463
|
+
msgs = [...msgs, ...result.toolCalls.map((tc, i) => ({ role: 'tool', tool_call_id: tc.id, content: contents[i] }))];
|
|
451
464
|
}
|
|
452
465
|
}
|
|
453
466
|
|
|
@@ -858,13 +871,17 @@ function publishContext(ai, context, send) {
|
|
|
858
871
|
confirm: (req) =>
|
|
859
872
|
new Promise((resolve) => {
|
|
860
873
|
const id = 'p' + crypto.randomBytes(4).toString('hex');
|
|
874
|
+
// the chat learns how the request ended (a click, or the two minutes passing), so the card never
|
|
875
|
+
// offers a button for a request the daemon has already forgotten
|
|
861
876
|
const timer = setTimeout(() => {
|
|
862
877
|
_pendingPublish.delete(id);
|
|
878
|
+
send?.({ type: 'publish_decided', id, decided: 'timeout' });
|
|
863
879
|
resolve('timeout');
|
|
864
880
|
}, PUBLISH_CONFIRM_MS);
|
|
865
881
|
_pendingPublish.set(id, (ok) => {
|
|
866
882
|
clearTimeout(timer);
|
|
867
883
|
_pendingPublish.delete(id);
|
|
884
|
+
send?.({ type: 'publish_decided', id, decided: ok ? 'published' : 'skipped' });
|
|
868
885
|
resolve(!!ok);
|
|
869
886
|
});
|
|
870
887
|
send?.({ type: 'publish_request', id, ...req });
|
|
@@ -874,7 +891,7 @@ function publishContext(ai, context, send) {
|
|
|
874
891
|
|
|
875
892
|
router.post('/publish/:id', (req, res) => {
|
|
876
893
|
const resolve = _pendingPublish.get(req.params.id);
|
|
877
|
-
if (!resolve) return res.status(
|
|
894
|
+
if (!resolve) return res.status(410).json({ error: 'this publish request was already answered or expired (two minutes); ask the assistant again' });
|
|
878
895
|
resolve(req.body?.ok === true);
|
|
879
896
|
res.json({ ok: true });
|
|
880
897
|
});
|
package/src/web/ai-context.js
CHANGED
|
@@ -23,7 +23,8 @@ const ROLE = read('role.md');
|
|
|
23
23
|
const CONVENTIONS = read('conventions.md');
|
|
24
24
|
const API_REF = read('api-ref.md');
|
|
25
25
|
const API_REF_COMPACT = read('api-ref-compact.md');
|
|
26
|
-
const FORMATS = read('formats.md');
|
|
26
|
+
const FORMATS = read('formats.md'); // the hand-over through propose_script (I16, I41)
|
|
27
|
+
const FORMATS_NOTOOLS = read('formats-notools.md'); // the fenced block with the @new-file hint, for chats without tools
|
|
27
28
|
const TOOLS = read('tools.md');
|
|
28
29
|
const COMPACT_STEPS = read('compact.md');
|
|
29
30
|
const DB_VIEW_PROMPT = read('db-view.md');
|
|
@@ -58,7 +59,7 @@ function houseSection(cfg, entries, instances, notes) {
|
|
|
58
59
|
const known = (instances || []).filter((i) => i.adapter).map((i) => `\`${i.instance}/\` ${i.adapter}${i.connected === 0 ? ' (offline)' : ''}`);
|
|
59
60
|
if (known.length) lines.push(`- adapter instances: ${known.slice(0, 20).join(', ')}${known.length > 20 ? ` … (${known.length})` : ''}`);
|
|
60
61
|
if (notes && notes.length) {
|
|
61
|
-
lines.push('-
|
|
62
|
+
lines.push('- your memory, facts from earlier chats (trust them over guesses; add with remember, drop with forget):');
|
|
62
63
|
for (const n of notes.slice(0, 300)) lines.push(` - [${n.id}] ${String(n.text).replace(/\s+/g, ' ').slice(0, 200)}`);
|
|
63
64
|
}
|
|
64
65
|
return lines.length > 1 ? lines.join('\n') : '';
|
|
@@ -98,7 +99,7 @@ function buildSystemPromptParts(requestCtx, currentScript, currentView, currentD
|
|
|
98
99
|
staticParts.push(ROLE, CONVENTIONS);
|
|
99
100
|
if (requestCtx.apiref !== false) staticParts.push(profile === 'compact' ? API_REF_COMPACT : API_REF);
|
|
100
101
|
if (toolsOffered) staticParts.push(TOOLS);
|
|
101
|
-
staticParts.push(FORMATS);
|
|
102
|
+
staticParts.push(toolsOffered ? FORMATS : FORMATS_NOTOOLS);
|
|
102
103
|
if (profile === 'compact') staticParts.push(COMPACT_STEPS);
|
|
103
104
|
}
|
|
104
105
|
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The worker side of the AI chat's run_analysis tool (roadmap I42, decision D-9). It runs the model's code as the
|
|
5
|
+
* body of an async function in a vm context that holds only `data`, `console` and the JavaScript built-ins —
|
|
6
|
+
* no require, no process, no network. Every `data.*` call is a message to the main thread, which answers from the
|
|
7
|
+
* daemon's loaders; the main thread kills the worker when the time is up.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const vm = require('node:vm');
|
|
11
|
+
const { parentPort, workerData } = require('node:worker_threads');
|
|
12
|
+
|
|
13
|
+
const MAX_LOG_LINES = 50;
|
|
14
|
+
const logs = [];
|
|
15
|
+
let nextId = 1;
|
|
16
|
+
const pending = new Map();
|
|
17
|
+
|
|
18
|
+
function call(name, args) {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const id = nextId++;
|
|
21
|
+
pending.set(id, { resolve, reject });
|
|
22
|
+
parentPort.postMessage({ type: 'call', id, name, args });
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
parentPort.on('message', (m) => {
|
|
27
|
+
if (m && m.type === 'reply') {
|
|
28
|
+
const p = pending.get(m.id);
|
|
29
|
+
if (!p) return;
|
|
30
|
+
pending.delete(m.id);
|
|
31
|
+
if (m.ok) p.resolve(m.value);
|
|
32
|
+
else p.reject(new Error(m.error));
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const fmt = (args) =>
|
|
37
|
+
args
|
|
38
|
+
.map((a) => {
|
|
39
|
+
if (typeof a === 'string') return a;
|
|
40
|
+
try {
|
|
41
|
+
return JSON.stringify(a);
|
|
42
|
+
} catch {
|
|
43
|
+
return String(a);
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
.join(' ');
|
|
47
|
+
const sandboxConsole = {
|
|
48
|
+
log: (...a) => logs.length < MAX_LOG_LINES && logs.push(fmt(a)),
|
|
49
|
+
info: (...a) => logs.length < MAX_LOG_LINES && logs.push(fmt(a)),
|
|
50
|
+
warn: (...a) => logs.length < MAX_LOG_LINES && logs.push('warn: ' + fmt(a)),
|
|
51
|
+
error: (...a) => logs.length < MAX_LOG_LINES && logs.push('error: ' + fmt(a)),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const data = Object.freeze({
|
|
55
|
+
topics: (filter = '') => call('topics', [filter]),
|
|
56
|
+
history: (topics, from, to) => call('history', [topics, from, to]),
|
|
57
|
+
messages: (topics, from, to, limit) => call('messages', [topics, from, to, limit]),
|
|
58
|
+
log: (filter = '', from, to, level, limit) => call('log', [filter, from, to, level, limit]),
|
|
59
|
+
script: (path) => call('script', [path]),
|
|
60
|
+
time: (str) => call('time', [str]),
|
|
61
|
+
from: workerData.from,
|
|
62
|
+
to: workerData.to,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const context = vm.createContext({ data, console: sandboxConsole });
|
|
66
|
+
|
|
67
|
+
(async () => {
|
|
68
|
+
let value;
|
|
69
|
+
try {
|
|
70
|
+
const script = new vm.Script('(async () => {\n' + String(workerData.code) + '\n})()', { filename: 'analysis.js' });
|
|
71
|
+
value = await script.runInContext(context, { timeout: workerData.timeoutMs });
|
|
72
|
+
} catch (e) {
|
|
73
|
+
parentPort.postMessage({ type: 'error', message: e && e.message ? e.message : String(e), logs });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
let json;
|
|
77
|
+
try {
|
|
78
|
+
json = JSON.stringify(value === undefined ? null : value);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
parentPort.postMessage({ type: 'error', message: 'the result is not JSON-serialisable: ' + e.message, logs });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
parentPort.postMessage({ type: 'done', json, logs });
|
|
84
|
+
})();
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The main-thread side of the AI chat's run_analysis tool (roadmap I42, decision D-9): starts the worker with the
|
|
5
|
+
* model's code, answers its data calls from the loaders it is given, and terminates it when the time is up or the
|
|
6
|
+
* heap limit is hit. Nothing of the daemon is reachable from the worker except through the loaders.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const path = require('node:path');
|
|
10
|
+
const { Worker } = require('node:worker_threads');
|
|
11
|
+
|
|
12
|
+
const DEFAULT_TIMEOUT_MS = 10000;
|
|
13
|
+
const DEFAULT_HEAP_MB = 256;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {{ code: string, from?: string, to?: string }} job
|
|
17
|
+
* @param {Record<string, (...args: any[]) => any>} loaders topics, history, messages, log, script, time
|
|
18
|
+
* @param {{ timeoutMs?: number, heapMb?: number }} [opts]
|
|
19
|
+
* @returns {Promise<{ json?: string, error?: string, logs: string[], ms: number, calls: number }>}
|
|
20
|
+
*/
|
|
21
|
+
function runAnalysis(job, loaders, opts = {}) {
|
|
22
|
+
const timeoutMs = Math.max(1000, Number(opts.timeoutMs) || DEFAULT_TIMEOUT_MS);
|
|
23
|
+
const heapMb = Math.max(32, Number(opts.heapMb) || DEFAULT_HEAP_MB);
|
|
24
|
+
const started = Date.now();
|
|
25
|
+
let calls = 0;
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
let settled = false;
|
|
28
|
+
const worker = new Worker(path.join(__dirname, 'ai-sandbox-worker.js'), {
|
|
29
|
+
workerData: { code: String(job.code || ''), from: job.from ?? null, to: job.to ?? null, timeoutMs },
|
|
30
|
+
resourceLimits: { maxOldGenerationSizeMb: heapMb, maxYoungGenerationSizeMb: 32 },
|
|
31
|
+
});
|
|
32
|
+
const finish = (result) => {
|
|
33
|
+
if (settled) return;
|
|
34
|
+
settled = true;
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
worker.terminate().catch(() => {});
|
|
37
|
+
resolve({ ...result, ms: Date.now() - started, calls });
|
|
38
|
+
};
|
|
39
|
+
const timer = setTimeout(() => finish({ error: `the analysis did not finish within ${timeoutMs / 1000} s and was stopped`, logs: [] }), timeoutMs);
|
|
40
|
+
worker.on('message', async (m) => {
|
|
41
|
+
if (!m || typeof m !== 'object') return;
|
|
42
|
+
if (m.type === 'call') {
|
|
43
|
+
calls++;
|
|
44
|
+
const fn = loaders[m.name];
|
|
45
|
+
try {
|
|
46
|
+
if (typeof fn !== 'function') throw new Error(`data.${m.name} is not available`);
|
|
47
|
+
const value = await fn(...(Array.isArray(m.args) ? m.args : []));
|
|
48
|
+
worker.postMessage({ type: 'reply', id: m.id, ok: true, value });
|
|
49
|
+
} catch (e) {
|
|
50
|
+
worker.postMessage({ type: 'reply', id: m.id, ok: false, error: e && e.message ? e.message : String(e) });
|
|
51
|
+
}
|
|
52
|
+
} else if (m.type === 'done') finish({ json: m.json, logs: m.logs || [] });
|
|
53
|
+
else if (m.type === 'error') finish({ error: m.message, logs: m.logs || [] });
|
|
54
|
+
});
|
|
55
|
+
worker.on('error', (e) => finish({ error: e && e.message ? e.message : String(e), logs: [] }));
|
|
56
|
+
worker.on('exit', (code) => {
|
|
57
|
+
if (!settled)
|
|
58
|
+
finish({ error: code === 0 ? 'the analysis ended without a result' : `the analysis was stopped (exit ${code}; the heap limit of ${heapMb} MB?)`, logs: [] });
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
module.exports = { runAnalysis, DEFAULT_TIMEOUT_MS };
|