iobroker.assistant 1.0.3 → 1.2.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/README.md +65 -0
- package/admin/custom/.vite/manifest.json +62 -62
- package/admin/custom/assets/Components-BSewCBfS.js +2 -0
- package/admin/custom/assets/{_virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-A4gakomR.js → _virtual_mf-localSharedImportMap___mfe_internal__ConfigCustomAssistant__mf_owner__1-C5NPM0Of.js} +1 -1
- package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__loadShare___mf_0_emotion_mf_1_styled__loadShare__.js-CwqmfMvn.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare___mf_0_emotion_mf_1_styled__loadShare__.js-IaosaymG.js} +1 -1
- package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-BMuLYe6f.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare___mf_0_iobroker_mf_1_gui_mf_2_components__loadShare__.js-Cdyjt5KT.js} +39 -39
- package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__loadShare__shared-BxjZmnpc.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare__shared-Dsw5zWS8.js} +1 -1
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__prebuild___mf_0_iobroker_mf_1_json_mf_2_config__prebuild__-SgSagTOv.js +122 -0
- package/admin/custom/assets/{hostInit-4roC4hEj.js → hostInit-jJe8bIfV.js} +1 -1
- package/admin/custom/assets/{lib-CchzgTUs.js → lib-CL8mXvI6.js} +1 -1
- package/admin/custom/assets/{mf-entry-bootstrap-0-4806bbe4.js → mf-entry-bootstrap-0-ab227aa6.js} +2 -2
- package/admin/custom/assets/{pendingShares-DLlv8yFY.js → pendingShares-wrGtfliQ.js} +1 -1
- package/admin/custom/customComponents.js +2 -2
- package/admin/custom/i18n/de.json +40 -0
- package/admin/custom/i18n/en.json +40 -0
- package/admin/custom/i18n/es.json +40 -0
- package/admin/custom/i18n/fr.json +40 -0
- package/admin/custom/i18n/it.json +40 -0
- package/admin/custom/i18n/nl.json +40 -0
- package/admin/custom/i18n/pl.json +40 -0
- package/admin/custom/i18n/pt.json +40 -0
- package/admin/custom/i18n/ru.json +40 -0
- package/admin/custom/i18n/uk.json +40 -0
- package/admin/custom/i18n/zh-cn.json +40 -0
- package/admin/custom/index.html +2 -2
- package/admin/custom/mf-manifest.json +1 -1
- package/admin/custom/mf-stats.json +1 -1
- package/admin/i18n/de.json +13 -3
- package/admin/i18n/en.json +13 -3
- package/admin/i18n/es.json +13 -3
- package/admin/i18n/fr.json +13 -3
- package/admin/i18n/it.json +13 -3
- package/admin/i18n/nl.json +13 -3
- package/admin/i18n/pl.json +13 -3
- package/admin/i18n/pt.json +13 -3
- package/admin/i18n/ru.json +13 -3
- package/admin/i18n/uk.json +13 -3
- package/admin/i18n/zh-cn.json +13 -3
- package/admin/jsonConfig.json +61 -2
- package/build/lib/journal.js +298 -2
- package/build/lib/journal.js.map +1 -1
- package/build/lib/llm.js +105 -4
- package/build/lib/llm.js.map +1 -1
- package/build/lib/localLlm.js +47 -10
- package/build/lib/localLlm.js.map +1 -1
- package/build/lib/nlu.js +124 -5
- package/build/lib/nlu.js.map +1 -1
- package/build/lib/tools.js +3 -0
- package/build/lib/tools.js.map +1 -1
- package/build/lib/voice/esphome.js +81 -7
- package/build/lib/voice/esphome.js.map +1 -1
- package/build/lib/voice/esphomeProto.js.map +1 -1
- package/build/lib/voice/mediaServer.js +29 -1
- package/build/lib/voice/mediaServer.js.map +1 -1
- package/build/lib/voice/protocol.js.map +1 -1
- package/build/lib/voice/voiceServer.js +81 -7
- package/build/lib/voice/voiceServer.js.map +1 -1
- package/build/lib/voice/wakeWords.js +284 -0
- package/build/lib/voice/wakeWords.js.map +1 -0
- package/build/lib/voice/wyoming.js +12 -4
- package/build/lib/voice/wyoming.js.map +1 -1
- package/build/lib/weather.js +160 -0
- package/build/lib/weather.js.map +1 -1
- package/build/lib/youtube.js +625 -0
- package/build/lib/youtube.js.map +1 -0
- package/build/main.js +1152 -76
- package/build/main.js.map +1 -1
- package/io-package.json +30 -14
- package/package.json +6 -6
- package/admin/custom/assets/Components-C6qcpaRC.js +0 -1
- package/admin/custom/assets/_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__prebuild___mf_0_iobroker_mf_1_json_mf_2_config__prebuild__-TM4l0OYm.js +0 -122
- /package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__loadShare___mf_0_emotion_mf_1_react__loadShare__.js-BkH7fzDw.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__loadShare___mf_0_emotion_mf_1_react__loadShare__.js-BkH7fzDw.js} +0 -0
- /package/admin/custom/assets/{_virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__201375093093590__prebuild__react_mf_1_jsx_mf_2_runtime__prebuild__-M61rjtmp.js → _virtual_mf___mfe_internal__ConfigCustomAssistant__mf_owner__163539842885036__prebuild__react_mf_1_jsx_mf_2_runtime__prebuild__-M61rjtmp.js} +0 -0
package/build/lib/journal.js
CHANGED
|
@@ -33,7 +33,10 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.Journal = void 0;
|
|
36
|
+
exports.Journal = exports.MAX_TRACES = void 0;
|
|
37
|
+
exports.traceDirFor = traceDirFor;
|
|
38
|
+
exports.snapshot = snapshot;
|
|
39
|
+
exports.errorInfo = errorInfo;
|
|
37
40
|
exports.transportForSource = transportForSource;
|
|
38
41
|
/**
|
|
39
42
|
* Conversation journal: one entry per request the assistant answered — where it came from (chat, script,
|
|
@@ -46,6 +49,12 @@ exports.transportForSource = transportForSource;
|
|
|
46
49
|
* {@link AsyncLocalStorage}: {@link Journal.run} opens it, and {@link Journal.note}/{@link Journal.tool}/
|
|
47
50
|
* {@link Journal.stateAccess} write to whatever entry the current call chain belongs to — and do nothing
|
|
48
51
|
* outside of one (a trigger writing a state is not a conversation).
|
|
52
|
+
*
|
|
53
|
+
* Besides the entry, every request keeps a **trace**: the steps of its processing with their timing and
|
|
54
|
+
* whatever explains them — the NLU's parse, the full prompt and every round of the model, tool arguments
|
|
55
|
+
* and results in full, speech recognition and synthesis. Traces are too big to ride along in `journal.last`
|
|
56
|
+
* and in `journal.json`, so the chat fetches one when a request is opened ({@link Journal.trace}); each is
|
|
57
|
+
* kept in a file of its own and only for the newest {@link MAX_TRACES} requests.
|
|
49
58
|
*/
|
|
50
59
|
const node_async_hooks_1 = require("node:async_hooks");
|
|
51
60
|
const node_fs_1 = require("node:fs");
|
|
@@ -54,6 +63,68 @@ const path = __importStar(require("node:path"));
|
|
|
54
63
|
const MAX_ENTRIES = 3000;
|
|
55
64
|
const SAVE_DELAY_MS = 5000;
|
|
56
65
|
const RESULT_CHARS = 300;
|
|
66
|
+
/** Requests whose trace is kept on disk. */
|
|
67
|
+
exports.MAX_TRACES = 200;
|
|
68
|
+
/** Traces held in memory (the newest ones — the ones somebody is about to open). */
|
|
69
|
+
const MEMORY_TRACES = 30;
|
|
70
|
+
/** Steps per request; a tool loop gone wild must not grow one trace without end. */
|
|
71
|
+
const MAX_STEPS = 400;
|
|
72
|
+
/** Longer strings are cut — enough for a system prompt with a few hundred devices. */
|
|
73
|
+
const STEP_STRING_CHARS = 100_000;
|
|
74
|
+
/** A reply is matched to its synthesis only this long after the answer. */
|
|
75
|
+
const SPEECH_WINDOW_MS = 120_000;
|
|
76
|
+
/** Entry ids as {@link Journal.run} makes them — checked before one becomes a file name. */
|
|
77
|
+
const ID_RE = /^[a-z0-9-]+$/;
|
|
78
|
+
/** Directory the traces of a journal file are kept in. */
|
|
79
|
+
function traceDirFor(file) {
|
|
80
|
+
return path.join(path.dirname(file), 'journal-trace');
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* JSON snapshot of a step's data — taken when it is recorded, because the objects go on changing (the
|
|
84
|
+
* model's message list grows, cache markers move). Long strings are cut and buffers summarised.
|
|
85
|
+
*/
|
|
86
|
+
function snapshot(value) {
|
|
87
|
+
if (value === undefined) {
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const text = JSON.stringify(value, (_key, v) => {
|
|
92
|
+
if (typeof v === 'string' && v.length > STEP_STRING_CHARS) {
|
|
93
|
+
return `${v.slice(0, STEP_STRING_CHARS)}… [+${v.length - STEP_STRING_CHARS} chars]`;
|
|
94
|
+
}
|
|
95
|
+
if (typeof v === 'bigint') {
|
|
96
|
+
return v.toString();
|
|
97
|
+
}
|
|
98
|
+
// Buffer.toJSON() has already run when the replacer sees it.
|
|
99
|
+
const b = v;
|
|
100
|
+
if (b && typeof b === 'object' && b.type === 'Buffer' && Array.isArray(b.data)) {
|
|
101
|
+
return `<${b.data.length} bytes>`;
|
|
102
|
+
}
|
|
103
|
+
return v;
|
|
104
|
+
});
|
|
105
|
+
return text === undefined ? undefined : JSON.parse(text);
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
return `<not serializable: ${e.message}>`;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/** Everything an error carries that helps — an API error's status and body, the stack. */
|
|
112
|
+
function errorInfo(e) {
|
|
113
|
+
if (!(e instanceof Error)) {
|
|
114
|
+
return { message: String(e) };
|
|
115
|
+
}
|
|
116
|
+
const info = { name: e.name, message: e.message };
|
|
117
|
+
for (const key of ['status', 'code', 'type', 'param', 'error', 'request_id', 'requestID']) {
|
|
118
|
+
const v = e[key];
|
|
119
|
+
if (v !== undefined && v !== null) {
|
|
120
|
+
info[key] = v;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (e.stack) {
|
|
124
|
+
info.stack = e.stack;
|
|
125
|
+
}
|
|
126
|
+
return info;
|
|
127
|
+
}
|
|
57
128
|
/** Derive the transport from a request source when the caller did not say. */
|
|
58
129
|
function transportForSource(source) {
|
|
59
130
|
if (source === 'chat') {
|
|
@@ -76,8 +147,16 @@ class Journal {
|
|
|
76
147
|
store = new node_async_hooks_1.AsyncLocalStorage();
|
|
77
148
|
saveTimer = null;
|
|
78
149
|
counter = 0;
|
|
150
|
+
/** Steps of the newest requests; older ones are read back from their file. */
|
|
151
|
+
traces = new Map();
|
|
152
|
+
/** Traces changed since they were last written. */
|
|
153
|
+
dirty = new Set();
|
|
154
|
+
/** LLM tool calls in progress per request: a state access inside one belongs to that tool's step. */
|
|
155
|
+
toolDepth = new WeakMap();
|
|
156
|
+
traceDir;
|
|
79
157
|
constructor(opts) {
|
|
80
158
|
this.opts = opts;
|
|
159
|
+
this.traceDir = opts.file ? traceDirFor(opts.file) : '';
|
|
81
160
|
}
|
|
82
161
|
get enabled() {
|
|
83
162
|
return this.opts.hours > 0;
|
|
@@ -110,8 +189,34 @@ class Journal {
|
|
|
110
189
|
}
|
|
111
190
|
clear() {
|
|
112
191
|
this.entries = [];
|
|
192
|
+
this.traces.clear();
|
|
193
|
+
this.dirty.clear();
|
|
113
194
|
this.scheduleSave();
|
|
114
195
|
}
|
|
196
|
+
/** The entry of a request (the chat asks for it together with its trace). */
|
|
197
|
+
get(id) {
|
|
198
|
+
return this.entries.find(e => e.id === id);
|
|
199
|
+
}
|
|
200
|
+
/** The processing steps of a request, or null when none were kept (too old, or from an older version). */
|
|
201
|
+
async trace(id) {
|
|
202
|
+
if (!ID_RE.test(id) || !this.get(id)) {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
const steps = this.traces.get(id);
|
|
206
|
+
if (steps) {
|
|
207
|
+
return steps;
|
|
208
|
+
}
|
|
209
|
+
if (!this.traceDir) {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const raw = JSON.parse(await node_fs_1.promises.readFile(path.join(this.traceDir, `${id}.json`), 'utf8'));
|
|
214
|
+
return Array.isArray(raw?.steps) ? raw.steps : null;
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
115
220
|
/**
|
|
116
221
|
* Run `fn` as one journalled request. The entry is published once when the request starts (so the
|
|
117
222
|
* chat shows the question while the answer is still being worked out) and once when it is finished.
|
|
@@ -131,7 +236,7 @@ class Journal {
|
|
|
131
236
|
read: [],
|
|
132
237
|
written: [],
|
|
133
238
|
};
|
|
134
|
-
for (const key of ['satellite', 'room', 'wakeWord', 'sttMs']) {
|
|
239
|
+
for (const key of ['satellite', 'room', 'wakeWord', 'sttMs', 'audioMs']) {
|
|
135
240
|
if (origin[key] !== undefined && origin[key] !== '') {
|
|
136
241
|
entry[key] = origin[key];
|
|
137
242
|
}
|
|
@@ -141,6 +246,12 @@ class Journal {
|
|
|
141
246
|
entry.wakeWord = '';
|
|
142
247
|
}
|
|
143
248
|
this.entries.push(entry);
|
|
249
|
+
this.traces.set(entry.id, []);
|
|
250
|
+
this.addStep(entry, {
|
|
251
|
+
stage: 'request',
|
|
252
|
+
title: `Request from ${source || entry.transport} (${entry.transport})`,
|
|
253
|
+
data: { question, source, origin },
|
|
254
|
+
});
|
|
144
255
|
this.publish(entry);
|
|
145
256
|
try {
|
|
146
257
|
const answer = await this.store.run(entry, fn);
|
|
@@ -149,16 +260,162 @@ class Journal {
|
|
|
149
260
|
}
|
|
150
261
|
catch (e) {
|
|
151
262
|
entry.error = e.message;
|
|
263
|
+
this.addStep(entry, { stage: 'error', level: 'error', title: entry.error, data: errorInfo(e) });
|
|
152
264
|
throw e;
|
|
153
265
|
}
|
|
154
266
|
finally {
|
|
155
267
|
entry.done = true;
|
|
156
268
|
entry.ms = Date.now() - entry.ts;
|
|
269
|
+
this.dirty.add(entry.id);
|
|
157
270
|
this.publish(entry);
|
|
158
271
|
this.prune();
|
|
159
272
|
this.scheduleSave();
|
|
160
273
|
}
|
|
161
274
|
}
|
|
275
|
+
/** Add a step to the current request's trace (nothing outside of one). */
|
|
276
|
+
step(input) {
|
|
277
|
+
const entry = this.store.getStore();
|
|
278
|
+
if (entry) {
|
|
279
|
+
this.addStep(entry, input);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Run an LLM tool as part of the current request: the call lands in the entry's tool list and, with the
|
|
284
|
+
* full arguments and result, in the trace.
|
|
285
|
+
*/
|
|
286
|
+
async toolCall(name, args, fn) {
|
|
287
|
+
const entry = this.store.getStore();
|
|
288
|
+
if (!entry) {
|
|
289
|
+
return fn();
|
|
290
|
+
}
|
|
291
|
+
this.toolDepth.set(entry, (this.toolDepth.get(entry) || 0) + 1);
|
|
292
|
+
const start = Date.now();
|
|
293
|
+
try {
|
|
294
|
+
const result = await fn();
|
|
295
|
+
const ms = Date.now() - start;
|
|
296
|
+
this.addStep(entry, { stage: 'tool', title: `Tool ${name}`, ms, data: { args, result } });
|
|
297
|
+
this.tool(name, args, ms, result);
|
|
298
|
+
return result;
|
|
299
|
+
}
|
|
300
|
+
catch (e) {
|
|
301
|
+
const ms = Date.now() - start;
|
|
302
|
+
const message = e.message;
|
|
303
|
+
this.addStep(entry, {
|
|
304
|
+
stage: 'tool',
|
|
305
|
+
level: 'error',
|
|
306
|
+
title: `Tool ${name} failed: ${message}`,
|
|
307
|
+
ms,
|
|
308
|
+
data: { args, error: errorInfo(e) },
|
|
309
|
+
});
|
|
310
|
+
this.tool(name, args, ms, undefined, message);
|
|
311
|
+
throw e;
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
this.toolDepth.set(entry, (this.toolDepth.get(entry) || 1) - 1);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Run a state call (MCP `get_states`/`set_state`/…) as part of the current request: the ids land in the
|
|
319
|
+
* entry's read/written lists, and — unless an LLM tool made the call, whose step already shows it — the
|
|
320
|
+
* call with its result in the trace.
|
|
321
|
+
*/
|
|
322
|
+
async stateCall(name, args, fn) {
|
|
323
|
+
const entry = this.store.getStore();
|
|
324
|
+
if (!entry) {
|
|
325
|
+
return fn();
|
|
326
|
+
}
|
|
327
|
+
this.stateAccess(name, args);
|
|
328
|
+
if (this.toolDepth.get(entry)) {
|
|
329
|
+
return fn();
|
|
330
|
+
}
|
|
331
|
+
const start = Date.now();
|
|
332
|
+
try {
|
|
333
|
+
const result = await fn();
|
|
334
|
+
this.addStep(entry, { stage: 'state', title: name, ms: Date.now() - start, data: { args, result } });
|
|
335
|
+
return result;
|
|
336
|
+
}
|
|
337
|
+
catch (e) {
|
|
338
|
+
this.addStep(entry, {
|
|
339
|
+
stage: 'state',
|
|
340
|
+
level: 'error',
|
|
341
|
+
title: `${name} failed: ${e.message}`,
|
|
342
|
+
ms: Date.now() - start,
|
|
343
|
+
data: { args, error: errorInfo(e) },
|
|
344
|
+
});
|
|
345
|
+
throw e;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* A transport synthesised the reply to the latest request of `source`. That happens after the answer,
|
|
350
|
+
* outside of the request, so the entry is found by its source.
|
|
351
|
+
*/
|
|
352
|
+
spoken(source, speech) {
|
|
353
|
+
const now = Date.now();
|
|
354
|
+
for (let i = this.entries.length - 1; i >= 0; i--) {
|
|
355
|
+
const entry = this.entries[i];
|
|
356
|
+
if (entry.source !== source) {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (!entry.done || entry.ttsMs !== undefined || now - entry.ts - (entry.ms || 0) > SPEECH_WINDOW_MS) {
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
entry.ttsMs = speech.ms;
|
|
363
|
+
entry.replyAudioMs = Math.round(speech.audioMs);
|
|
364
|
+
const voice = speech.voice ? ` (${speech.voice})` : '';
|
|
365
|
+
this.addStep(entry, {
|
|
366
|
+
stage: 'tts',
|
|
367
|
+
title: `Speech synthesis: ${speech.provider}${voice}, ${(speech.audioMs / 1000).toFixed(1)} s of audio`,
|
|
368
|
+
ms: speech.ms,
|
|
369
|
+
data: speech,
|
|
370
|
+
});
|
|
371
|
+
this.dirty.add(entry.id);
|
|
372
|
+
this.publish(entry);
|
|
373
|
+
this.scheduleSave();
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
addStep(entry, input) {
|
|
378
|
+
const steps = this.traces.get(entry.id);
|
|
379
|
+
if (!steps || steps.length > MAX_STEPS) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
if (steps.length === MAX_STEPS) {
|
|
383
|
+
steps.push({
|
|
384
|
+
t: Date.now() - entry.ts,
|
|
385
|
+
stage: 'error',
|
|
386
|
+
level: 'warn',
|
|
387
|
+
title: `More than ${MAX_STEPS} steps — the rest is not recorded`,
|
|
388
|
+
});
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const step = {
|
|
392
|
+
t: Date.now() - entry.ts - (input.ms || 0),
|
|
393
|
+
stage: input.stage,
|
|
394
|
+
title: input.title,
|
|
395
|
+
};
|
|
396
|
+
if (input.ms !== undefined) {
|
|
397
|
+
step.ms = input.ms;
|
|
398
|
+
}
|
|
399
|
+
if (input.level) {
|
|
400
|
+
step.level = input.level;
|
|
401
|
+
}
|
|
402
|
+
if (input.tokens) {
|
|
403
|
+
step.tokens = input.tokens;
|
|
404
|
+
const sum = (entry.tokens ||= { input: 0, output: 0 });
|
|
405
|
+
sum.input += input.tokens.input || 0;
|
|
406
|
+
sum.output += input.tokens.output || 0;
|
|
407
|
+
if (input.tokens.cacheRead) {
|
|
408
|
+
sum.cacheRead = (sum.cacheRead || 0) + input.tokens.cacheRead;
|
|
409
|
+
}
|
|
410
|
+
if (input.tokens.cacheWrite) {
|
|
411
|
+
sum.cacheWrite = (sum.cacheWrite || 0) + input.tokens.cacheWrite;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (input.data !== undefined) {
|
|
415
|
+
step.data = snapshot(input.data);
|
|
416
|
+
}
|
|
417
|
+
steps.push(step);
|
|
418
|
+
}
|
|
162
419
|
/** Fill in fields of the current request's entry (tier, detail, followUp …). */
|
|
163
420
|
note(fields) {
|
|
164
421
|
const entry = this.store.getStore();
|
|
@@ -243,8 +500,23 @@ class Journal {
|
|
|
243
500
|
}
|
|
244
501
|
start = Math.max(start, this.entries.length - MAX_ENTRIES);
|
|
245
502
|
if (start > 0) {
|
|
503
|
+
for (const e of this.entries.slice(0, start)) {
|
|
504
|
+
this.traces.delete(e.id);
|
|
505
|
+
this.dirty.delete(e.id);
|
|
506
|
+
}
|
|
246
507
|
this.entries = this.entries.slice(start);
|
|
247
508
|
}
|
|
509
|
+
// Without a file the memory is all there is: keep as many as would be on disk.
|
|
510
|
+
const keep = this.traceDir ? MEMORY_TRACES : exports.MAX_TRACES;
|
|
511
|
+
for (const id of this.traces.keys()) {
|
|
512
|
+
if (this.traces.size <= keep) {
|
|
513
|
+
break;
|
|
514
|
+
}
|
|
515
|
+
// Not written yet, or still being answered: needed until it is on disk.
|
|
516
|
+
if ((!this.traceDir || !this.dirty.has(id)) && this.get(id)?.done !== false) {
|
|
517
|
+
this.traces.delete(id);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
248
520
|
}
|
|
249
521
|
scheduleSave() {
|
|
250
522
|
if (!this.opts.file || this.saveTimer) {
|
|
@@ -267,6 +539,30 @@ class Journal {
|
|
|
267
539
|
catch (e) {
|
|
268
540
|
this.opts.log.warn(`Journal could not be saved: ${e.message}`);
|
|
269
541
|
}
|
|
542
|
+
await this.saveTraces();
|
|
543
|
+
this.prune();
|
|
544
|
+
}
|
|
545
|
+
/** Write the traces that changed, and delete the files of requests that are no longer kept. */
|
|
546
|
+
async saveTraces() {
|
|
547
|
+
try {
|
|
548
|
+
await node_fs_1.promises.mkdir(this.traceDir, { recursive: true });
|
|
549
|
+
for (const id of [...this.dirty]) {
|
|
550
|
+
const steps = this.traces.get(id);
|
|
551
|
+
this.dirty.delete(id);
|
|
552
|
+
if (steps) {
|
|
553
|
+
await node_fs_1.promises.writeFile(path.join(this.traceDir, `${id}.json`), JSON.stringify({ id, steps }));
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
const keep = new Set(this.entries.slice(-exports.MAX_TRACES).map(e => e.id));
|
|
557
|
+
for (const name of await node_fs_1.promises.readdir(this.traceDir)) {
|
|
558
|
+
if (name.endsWith('.json') && !keep.has(name.slice(0, -5))) {
|
|
559
|
+
await node_fs_1.promises.rm(path.join(this.traceDir, name), { force: true });
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
catch (e) {
|
|
564
|
+
this.opts.log.warn(`Journal traces could not be saved: ${e.message}`);
|
|
565
|
+
}
|
|
270
566
|
}
|
|
271
567
|
}
|
|
272
568
|
exports.Journal = Journal;
|
package/build/lib/journal.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"journal.js","sourceRoot":"","sources":["../../src/lib/journal.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Conversation journal: one entry per request the assistant answered — where it came from (chat, script,\n * Telegram, which satellite, which wake word), which tier answered it (routine, rule-based NLU, local\n * model, cloud model), which tools the model called and which state ids were read or written along the\n * way. The admin chat shows it live, so one can check what a satellite heard and what was done about it.\n *\n * Tools and state accesses happen deep inside the pipeline (NLU executor, LLM tool loop, routine\n * executor). Instead of threading an entry object through all of them, the entry rides along in an\n * {@link AsyncLocalStorage}: {@link Journal.run} opens it, and {@link Journal.note}/{@link Journal.tool}/\n * {@link Journal.stateAccess} write to whatever entry the current call chain belongs to — and do nothing\n * outside of one (a trigger writing a state is not a conversation).\n */\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport { promises as fs } from 'node:fs';\nimport * as path from 'node:path';\n\n/** How a request reached us. */\nexport type JournalTransport = 'chat' | 'script' | 'state' | 'telegram' | 'esphome' | 'udp' | 'native' | 'wyoming';\n\n/** Which stage of the pipeline produced the answer. */\nexport type JournalTier =\n | 'stop' // stop word silenced a ringing timer/alarm\n | 'askReply' // the utterance answered an open `askUser` question\n | 'routine'\n | 'nlu'\n | 'localLlm'\n | 'cloud'\n | 'none'; // nothing local understood it and there is no cloud provider\n\nexport interface JournalTool {\n name: string;\n args: Record<string, unknown>;\n ms: number;\n /** Result, JSON, shortened. */\n result?: string;\n error?: string;\n}\n\nexport interface JournalEntry {\n id: string;\n /** Start of the answer, ms. */\n ts: number;\n /** False while the answer is still being produced. */\n done: boolean;\n /** Time to produce the answer (without STT/TTS). */\n ms?: number;\n source: string;\n transport: JournalTransport;\n satellite?: string;\n room?: string;\n /** Wake word the satellite reported; '' = the mic was re-opened for a follow-up, absent = unknown. */\n wakeWord?: string;\n /** Speech recognition time, for voice requests. */\n sttMs?: number;\n question: string;\n answer?: string;\n error?: string;\n tier?: JournalTier;\n /** Model name, routine name or NLU actions — whatever explains the tier. */\n detail?: string;\n tools: JournalTool[];\n /** State ids read to produce the answer. */\n read: string[];\n /** States written to produce the answer. */\n written: { id: string; value: unknown }[];\n /** The reply asks back, so a satellite re-opened its mic. */\n followUp?: boolean;\n}\n\n/** What the caller knows about a request before the pipeline starts. */\nexport interface JournalOrigin {\n transport?: JournalTransport;\n satellite?: string;\n room?: string;\n wakeWord?: string;\n sttMs?: number;\n}\n\nexport interface JournalOptions {\n /** Retention in hours; 0 disables the journal entirely. */\n hours: number;\n /** File the journal is kept in across restarts ('' = memory only). */\n file: string;\n /** Called with every new or updated entry (drives the `journal.last` state the chat listens to). */\n onChange?: (entry: JournalEntry) => void;\n log: ioBroker.Logger;\n}\n\n/** Hard cap independent of the retention — a script asking every few seconds must not fill the disk. */\nconst MAX_ENTRIES = 3000;\nconst SAVE_DELAY_MS = 5000;\nconst RESULT_CHARS = 300;\n\n/** Derive the transport from a request source when the caller did not say. */\nexport function transportForSource(source: string): JournalTransport {\n if (source === 'chat') {\n return 'chat';\n }\n if (source === '') {\n return 'state';\n }\n if (source === 'wyoming') {\n return 'wyoming';\n }\n if (source.startsWith('telegram:')) {\n return 'telegram';\n }\n return 'script';\n}\n\nexport class Journal {\n private entries: JournalEntry[] = [];\n private readonly store = new AsyncLocalStorage<JournalEntry>();\n private saveTimer: NodeJS.Timeout | null = null;\n private counter = 0;\n\n constructor(private readonly opts: JournalOptions) {}\n\n get enabled(): boolean {\n return this.opts.hours > 0;\n }\n\n /** Load the entries of the last run (silently starts empty when there are none). */\n async load(): Promise<void> {\n if (!this.enabled || !this.opts.file) {\n return;\n }\n try {\n const raw = JSON.parse(await fs.readFile(this.opts.file, 'utf8')) as unknown;\n if (Array.isArray(raw)) {\n // An entry still open at shutdown never got its answer.\n this.entries = (raw as JournalEntry[])\n .filter(e => e && typeof e.id === 'string' && typeof e.ts === 'number')\n .map(e => (e.done ? e : { ...e, done: true, error: e.error || 'interrupted' }));\n this.prune();\n }\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {\n this.opts.log.warn(`Journal could not be read: ${(e as Error).message}`);\n }\n }\n }\n\n /** Entries newer than `since` (ms), oldest first. */\n list(since = 0): JournalEntry[] {\n this.prune();\n return this.entries.filter(e => e.ts > since);\n }\n\n clear(): void {\n this.entries = [];\n this.scheduleSave();\n }\n\n /**\n * Run `fn` as one journalled request. The entry is published once when the request starts (so the\n * chat shows the question while the answer is still being worked out) and once when it is finished.\n */\n async run(question: string, source: string, origin: JournalOrigin, fn: () => Promise<string>): Promise<string> {\n if (!this.enabled) {\n return fn();\n }\n const entry: JournalEntry = {\n id: `${Date.now().toString(36)}-${(this.counter++).toString(36)}`,\n ts: Date.now(),\n done: false,\n source,\n transport: origin.transport || transportForSource(source),\n question,\n tools: [],\n read: [],\n written: [],\n };\n for (const key of ['satellite', 'room', 'wakeWord', 'sttMs'] as const) {\n if (origin[key] !== undefined && origin[key] !== '') {\n (entry as unknown as Record<string, unknown>)[key] = origin[key];\n }\n }\n // A satellite that re-opened its mic reports no wake word — say so instead of \"unknown\".\n if (origin.wakeWord === '') {\n entry.wakeWord = '';\n }\n this.entries.push(entry);\n this.publish(entry);\n try {\n const answer = await this.store.run(entry, fn);\n entry.answer = answer;\n return answer;\n } catch (e) {\n entry.error = (e as Error).message;\n throw e;\n } finally {\n entry.done = true;\n entry.ms = Date.now() - entry.ts;\n this.publish(entry);\n this.prune();\n this.scheduleSave();\n }\n }\n\n /** Fill in fields of the current request's entry (tier, detail, followUp …). */\n note(fields: Partial<Pick<JournalEntry, 'tier' | 'detail' | 'followUp'>>): void {\n const entry = this.store.getStore();\n if (entry) {\n Object.assign(entry, fields);\n }\n }\n\n /** Record a tool call the model made for the current request. */\n tool(name: string, args: Record<string, unknown>, ms: number, result: unknown, error?: string): void {\n const entry = this.store.getStore();\n if (!entry) {\n return;\n }\n const item: JournalTool = { name, args: args || {}, ms };\n if (error) {\n item.error = error;\n } else {\n let text: string;\n try {\n text = typeof result === 'string' ? result : JSON.stringify(result);\n } catch {\n text = String(result);\n }\n item.result = text.length > RESULT_CHARS ? `${text.slice(0, RESULT_CHARS)}…` : text;\n }\n entry.tools.push(item);\n // Live view: show each tool call as it happens, a cloud round can take seconds.\n this.publish(entry);\n }\n\n /** Record which states a call reads or writes (MCP `get_states`/`set_state`/`set_states`, routines). */\n stateAccess(tool: string, args: Record<string, unknown> | undefined): void {\n const entry = this.store.getStore();\n if (!entry || !args) {\n return;\n }\n if (tool === 'get_states' && Array.isArray(args.ids)) {\n for (const id of args.ids) {\n if (typeof id === 'string' && !entry.read.includes(id)) {\n entry.read.push(id);\n }\n }\n } else if (tool === 'get_state' && typeof args.id === 'string') {\n if (!entry.read.includes(args.id)) {\n entry.read.push(args.id);\n }\n } else if (tool === 'set_state' && typeof args.id === 'string') {\n entry.written.push({ id: args.id, value: args.value });\n } else if (tool === 'set_states' && Array.isArray(args.states)) {\n for (const s of args.states as { id?: unknown; value?: unknown }[]) {\n if (typeof s?.id === 'string') {\n entry.written.push({ id: s.id, value: s.value });\n }\n }\n }\n }\n\n /** Write pending changes now (on unload). */\n async flush(): Promise<void> {\n if (this.saveTimer) {\n clearTimeout(this.saveTimer);\n this.saveTimer = null;\n }\n await this.save();\n }\n\n private publish(entry: JournalEntry): void {\n try {\n this.opts.onChange?.(entry);\n } catch {\n // a listener must never break the answer\n }\n }\n\n private prune(): void {\n const cutoff = Date.now() - this.opts.hours * 3600_000;\n let start = 0;\n while (start < this.entries.length && this.entries[start].ts < cutoff) {\n start++;\n }\n start = Math.max(start, this.entries.length - MAX_ENTRIES);\n if (start > 0) {\n this.entries = this.entries.slice(start);\n }\n }\n\n private scheduleSave(): void {\n if (!this.opts.file || this.saveTimer) {\n return;\n }\n this.saveTimer = setTimeout(() => {\n this.saveTimer = null;\n void this.save();\n }, SAVE_DELAY_MS);\n this.saveTimer.unref?.();\n }\n\n private async save(): Promise<void> {\n if (!this.opts.file) {\n return;\n }\n try {\n await fs.mkdir(path.dirname(this.opts.file), { recursive: true });\n await fs.writeFile(this.opts.file, JSON.stringify(this.entries.filter(e => e.done)));\n } catch (e) {\n this.opts.log.warn(`Journal could not be saved: ${(e as Error).message}`);\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"journal.js","sourceRoot":"","sources":["../../src/lib/journal.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Conversation journal: one entry per request the assistant answered — where it came from (chat, script,\n * Telegram, which satellite, which wake word), which tier answered it (routine, rule-based NLU, local\n * model, cloud model), which tools the model called and which state ids were read or written along the\n * way. The admin chat shows it live, so one can check what a satellite heard and what was done about it.\n *\n * Tools and state accesses happen deep inside the pipeline (NLU executor, LLM tool loop, routine\n * executor). Instead of threading an entry object through all of them, the entry rides along in an\n * {@link AsyncLocalStorage}: {@link Journal.run} opens it, and {@link Journal.note}/{@link Journal.tool}/\n * {@link Journal.stateAccess} write to whatever entry the current call chain belongs to — and do nothing\n * outside of one (a trigger writing a state is not a conversation).\n *\n * Besides the entry, every request keeps a **trace**: the steps of its processing with their timing and\n * whatever explains them — the NLU's parse, the full prompt and every round of the model, tool arguments\n * and results in full, speech recognition and synthesis. Traces are too big to ride along in `journal.last`\n * and in `journal.json`, so the chat fetches one when a request is opened ({@link Journal.trace}); each is\n * kept in a file of its own and only for the newest {@link MAX_TRACES} requests.\n */\nimport { AsyncLocalStorage } from 'node:async_hooks';\nimport { promises as fs } from 'node:fs';\nimport * as path from 'node:path';\n\n/** How a request reached us. */\nexport type JournalTransport = 'chat' | 'script' | 'state' | 'telegram' | 'esphome' | 'udp' | 'native' | 'wyoming';\n\n/** Which stage of the pipeline produced the answer. */\nexport type JournalTier =\n | 'stop' // stop word silenced a ringing timer/alarm\n | 'askReply' // the utterance answered an open `askUser` question\n | 'routine'\n | 'nlu'\n | 'localLlm'\n | 'cloud'\n | 'none'; // nothing local understood it and there is no cloud provider\n\n/** Tokens a model call used (summed over the rounds of a request in {@link JournalEntry.tokens}). */\nexport interface JournalTokens {\n input: number;\n output: number;\n /** Prompt tokens served from the provider's prompt cache. */\n cacheRead?: number;\n /** Prompt tokens written to the provider's prompt cache (Anthropic). */\n cacheWrite?: number;\n}\n\n/** One step of how a request was processed — what the debug view of the admin chat shows. */\nexport interface JournalStep {\n /** Start of the step in ms after the request started (negative for speech recognition, which comes first). */\n t: number;\n /** Duration, for steps that took time. */\n ms?: number;\n /** Pipeline stage: request, stt, context, stop, askReply, routine, youtube, nlu, localLlm, cloud, tool, state, answer, tts, error. */\n stage: string;\n title: string;\n level?: 'warn' | 'error';\n tokens?: JournalTokens;\n /** Whatever explains the step (prompts, model responses, parse results) — a JSON snapshot. */\n data?: unknown;\n}\n\n/** What a caller records; the journal adds the timing. */\nexport type JournalStepInput = Omit<JournalStep, 't'>;\n\n/** Hook for library code that should not know the journal (the LLM agent). */\nexport type TraceFn = (step: JournalStepInput) => void;\n\nexport interface JournalTool {\n name: string;\n args: Record<string, unknown>;\n ms: number;\n /** Result, JSON, shortened. */\n result?: string;\n error?: string;\n}\n\nexport interface JournalEntry {\n id: string;\n /** Start of the answer, ms. */\n ts: number;\n /** False while the answer is still being produced. */\n done: boolean;\n /** Time to produce the answer (without STT/TTS). */\n ms?: number;\n source: string;\n transport: JournalTransport;\n satellite?: string;\n room?: string;\n /** Wake word the satellite reported; '' = the mic was re-opened for a follow-up, absent = unknown. */\n wakeWord?: string;\n /** Speech recognition time, for voice requests. */\n sttMs?: number;\n /** Length of the recorded utterance. */\n audioMs?: number;\n /** Speech synthesis time of the reply (voice requests, filled in after the answer). */\n ttsMs?: number;\n /** Length of the spoken reply. */\n replyAudioMs?: number;\n question: string;\n answer?: string;\n error?: string;\n tier?: JournalTier;\n /** Model name, routine name or NLU actions — whatever explains the tier. */\n detail?: string;\n tools: JournalTool[];\n /** State ids read to produce the answer. */\n read: string[];\n /** States written to produce the answer. */\n written: { id: string; value: unknown }[];\n /** The reply asks back, so a satellite re-opened its mic. */\n followUp?: boolean;\n /** Tokens of all model calls, when a cloud model answered. */\n tokens?: JournalTokens;\n}\n\n/** What the caller knows about a request before the pipeline starts. */\nexport interface JournalOrigin {\n transport?: JournalTransport;\n satellite?: string;\n room?: string;\n wakeWord?: string;\n sttMs?: number;\n audioMs?: number;\n /** Voice activity detection of the utterance (ESPHome) — trace only. */\n vad?: { peak: number; noiseFloor: number; startLevel: number; endLevel: number };\n}\n\n/** What a transport reports after it synthesised the reply. */\nexport interface JournalSpeech {\n ms: number;\n audioMs: number;\n provider: string;\n fallback?: string;\n voice?: string;\n}\n\nexport interface JournalOptions {\n /** Retention in hours; 0 disables the journal entirely. */\n hours: number;\n /** File the journal is kept in across restarts ('' = memory only). */\n file: string;\n /** Called with every new or updated entry (drives the `journal.last` state the chat listens to). */\n onChange?: (entry: JournalEntry) => void;\n log: ioBroker.Logger;\n}\n\n/** Hard cap independent of the retention — a script asking every few seconds must not fill the disk. */\nconst MAX_ENTRIES = 3000;\nconst SAVE_DELAY_MS = 5000;\nconst RESULT_CHARS = 300;\n/** Requests whose trace is kept on disk. */\nexport const MAX_TRACES = 200;\n/** Traces held in memory (the newest ones — the ones somebody is about to open). */\nconst MEMORY_TRACES = 30;\n/** Steps per request; a tool loop gone wild must not grow one trace without end. */\nconst MAX_STEPS = 400;\n/** Longer strings are cut — enough for a system prompt with a few hundred devices. */\nconst STEP_STRING_CHARS = 100_000;\n/** A reply is matched to its synthesis only this long after the answer. */\nconst SPEECH_WINDOW_MS = 120_000;\n/** Entry ids as {@link Journal.run} makes them — checked before one becomes a file name. */\nconst ID_RE = /^[a-z0-9-]+$/;\n\n/** Directory the traces of a journal file are kept in. */\nexport function traceDirFor(file: string): string {\n return path.join(path.dirname(file), 'journal-trace');\n}\n\n/**\n * JSON snapshot of a step's data — taken when it is recorded, because the objects go on changing (the\n * model's message list grows, cache markers move). Long strings are cut and buffers summarised.\n */\nexport function snapshot(value: unknown): unknown {\n if (value === undefined) {\n return undefined;\n }\n try {\n const text = JSON.stringify(value, (_key, v: unknown) => {\n if (typeof v === 'string' && v.length > STEP_STRING_CHARS) {\n return `${v.slice(0, STEP_STRING_CHARS)}… [+${v.length - STEP_STRING_CHARS} chars]`;\n }\n if (typeof v === 'bigint') {\n return v.toString();\n }\n // Buffer.toJSON() has already run when the replacer sees it.\n const b = v as { type?: unknown; data?: unknown } | null;\n if (b && typeof b === 'object' && b.type === 'Buffer' && Array.isArray(b.data)) {\n return `<${b.data.length} bytes>`;\n }\n return v;\n });\n return text === undefined ? undefined : JSON.parse(text);\n } catch (e) {\n return `<not serializable: ${(e as Error).message}>`;\n }\n}\n\n/** Everything an error carries that helps — an API error's status and body, the stack. */\nexport function errorInfo(e: unknown): Record<string, unknown> {\n if (!(e instanceof Error)) {\n return { message: String(e) };\n }\n const info: Record<string, unknown> = { name: e.name, message: e.message };\n for (const key of ['status', 'code', 'type', 'param', 'error', 'request_id', 'requestID']) {\n const v = (e as unknown as Record<string, unknown>)[key];\n if (v !== undefined && v !== null) {\n info[key] = v;\n }\n }\n if (e.stack) {\n info.stack = e.stack;\n }\n return info;\n}\n\n/** Derive the transport from a request source when the caller did not say. */\nexport function transportForSource(source: string): JournalTransport {\n if (source === 'chat') {\n return 'chat';\n }\n if (source === '') {\n return 'state';\n }\n if (source === 'wyoming') {\n return 'wyoming';\n }\n if (source.startsWith('telegram:')) {\n return 'telegram';\n }\n return 'script';\n}\n\nexport class Journal {\n private entries: JournalEntry[] = [];\n private readonly store = new AsyncLocalStorage<JournalEntry>();\n private saveTimer: NodeJS.Timeout | null = null;\n private counter = 0;\n /** Steps of the newest requests; older ones are read back from their file. */\n private readonly traces = new Map<string, JournalStep[]>();\n /** Traces changed since they were last written. */\n private readonly dirty = new Set<string>();\n /** LLM tool calls in progress per request: a state access inside one belongs to that tool's step. */\n private readonly toolDepth = new WeakMap<JournalEntry, number>();\n private readonly traceDir: string;\n\n constructor(private readonly opts: JournalOptions) {\n this.traceDir = opts.file ? traceDirFor(opts.file) : '';\n }\n\n get enabled(): boolean {\n return this.opts.hours > 0;\n }\n\n /** Load the entries of the last run (silently starts empty when there are none). */\n async load(): Promise<void> {\n if (!this.enabled || !this.opts.file) {\n return;\n }\n try {\n const raw = JSON.parse(await fs.readFile(this.opts.file, 'utf8')) as unknown;\n if (Array.isArray(raw)) {\n // An entry still open at shutdown never got its answer.\n this.entries = (raw as JournalEntry[])\n .filter(e => e && typeof e.id === 'string' && typeof e.ts === 'number')\n .map(e => (e.done ? e : { ...e, done: true, error: e.error || 'interrupted' }));\n this.prune();\n }\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code !== 'ENOENT') {\n this.opts.log.warn(`Journal could not be read: ${(e as Error).message}`);\n }\n }\n }\n\n /** Entries newer than `since` (ms), oldest first. */\n list(since = 0): JournalEntry[] {\n this.prune();\n return this.entries.filter(e => e.ts > since);\n }\n\n clear(): void {\n this.entries = [];\n this.traces.clear();\n this.dirty.clear();\n this.scheduleSave();\n }\n\n /** The entry of a request (the chat asks for it together with its trace). */\n get(id: string): JournalEntry | undefined {\n return this.entries.find(e => e.id === id);\n }\n\n /** The processing steps of a request, or null when none were kept (too old, or from an older version). */\n async trace(id: string): Promise<JournalStep[] | null> {\n if (!ID_RE.test(id) || !this.get(id)) {\n return null;\n }\n const steps = this.traces.get(id);\n if (steps) {\n return steps;\n }\n if (!this.traceDir) {\n return null;\n }\n try {\n const raw = JSON.parse(await fs.readFile(path.join(this.traceDir, `${id}.json`), 'utf8')) as {\n steps?: unknown;\n };\n return Array.isArray(raw?.steps) ? (raw.steps as JournalStep[]) : null;\n } catch {\n return null;\n }\n }\n\n /**\n * Run `fn` as one journalled request. The entry is published once when the request starts (so the\n * chat shows the question while the answer is still being worked out) and once when it is finished.\n */\n async run(question: string, source: string, origin: JournalOrigin, fn: () => Promise<string>): Promise<string> {\n if (!this.enabled) {\n return fn();\n }\n const entry: JournalEntry = {\n id: `${Date.now().toString(36)}-${(this.counter++).toString(36)}`,\n ts: Date.now(),\n done: false,\n source,\n transport: origin.transport || transportForSource(source),\n question,\n tools: [],\n read: [],\n written: [],\n };\n for (const key of ['satellite', 'room', 'wakeWord', 'sttMs', 'audioMs'] as const) {\n if (origin[key] !== undefined && origin[key] !== '') {\n (entry as unknown as Record<string, unknown>)[key] = origin[key];\n }\n }\n // A satellite that re-opened its mic reports no wake word — say so instead of \"unknown\".\n if (origin.wakeWord === '') {\n entry.wakeWord = '';\n }\n this.entries.push(entry);\n this.traces.set(entry.id, []);\n this.addStep(entry, {\n stage: 'request',\n title: `Request from ${source || entry.transport} (${entry.transport})`,\n data: { question, source, origin },\n });\n this.publish(entry);\n try {\n const answer = await this.store.run(entry, fn);\n entry.answer = answer;\n return answer;\n } catch (e) {\n entry.error = (e as Error).message;\n this.addStep(entry, { stage: 'error', level: 'error', title: entry.error, data: errorInfo(e) });\n throw e;\n } finally {\n entry.done = true;\n entry.ms = Date.now() - entry.ts;\n this.dirty.add(entry.id);\n this.publish(entry);\n this.prune();\n this.scheduleSave();\n }\n }\n\n /** Add a step to the current request's trace (nothing outside of one). */\n step(input: JournalStepInput): void {\n const entry = this.store.getStore();\n if (entry) {\n this.addStep(entry, input);\n }\n }\n\n /**\n * Run an LLM tool as part of the current request: the call lands in the entry's tool list and, with the\n * full arguments and result, in the trace.\n */\n async toolCall<T>(name: string, args: Record<string, unknown>, fn: () => Promise<T>): Promise<T> {\n const entry = this.store.getStore();\n if (!entry) {\n return fn();\n }\n this.toolDepth.set(entry, (this.toolDepth.get(entry) || 0) + 1);\n const start = Date.now();\n try {\n const result = await fn();\n const ms = Date.now() - start;\n this.addStep(entry, { stage: 'tool', title: `Tool ${name}`, ms, data: { args, result } });\n this.tool(name, args, ms, result);\n return result;\n } catch (e) {\n const ms = Date.now() - start;\n const message = (e as Error).message;\n this.addStep(entry, {\n stage: 'tool',\n level: 'error',\n title: `Tool ${name} failed: ${message}`,\n ms,\n data: { args, error: errorInfo(e) },\n });\n this.tool(name, args, ms, undefined, message);\n throw e;\n } finally {\n this.toolDepth.set(entry, (this.toolDepth.get(entry) || 1) - 1);\n }\n }\n\n /**\n * Run a state call (MCP `get_states`/`set_state`/…) as part of the current request: the ids land in the\n * entry's read/written lists, and — unless an LLM tool made the call, whose step already shows it — the\n * call with its result in the trace.\n */\n async stateCall<T>(name: string, args: Record<string, unknown> | undefined, fn: () => Promise<T>): Promise<T> {\n const entry = this.store.getStore();\n if (!entry) {\n return fn();\n }\n this.stateAccess(name, args);\n if (this.toolDepth.get(entry)) {\n return fn();\n }\n const start = Date.now();\n try {\n const result = await fn();\n this.addStep(entry, { stage: 'state', title: name, ms: Date.now() - start, data: { args, result } });\n return result;\n } catch (e) {\n this.addStep(entry, {\n stage: 'state',\n level: 'error',\n title: `${name} failed: ${(e as Error).message}`,\n ms: Date.now() - start,\n data: { args, error: errorInfo(e) },\n });\n throw e;\n }\n }\n\n /**\n * A transport synthesised the reply to the latest request of `source`. That happens after the answer,\n * outside of the request, so the entry is found by its source.\n */\n spoken(source: string, speech: JournalSpeech): void {\n const now = Date.now();\n for (let i = this.entries.length - 1; i >= 0; i--) {\n const entry = this.entries[i];\n if (entry.source !== source) {\n continue;\n }\n if (!entry.done || entry.ttsMs !== undefined || now - entry.ts - (entry.ms || 0) > SPEECH_WINDOW_MS) {\n return;\n }\n entry.ttsMs = speech.ms;\n entry.replyAudioMs = Math.round(speech.audioMs);\n const voice = speech.voice ? ` (${speech.voice})` : '';\n this.addStep(entry, {\n stage: 'tts',\n title: `Speech synthesis: ${speech.provider}${voice}, ${(speech.audioMs / 1000).toFixed(1)} s of audio`,\n ms: speech.ms,\n data: speech,\n });\n this.dirty.add(entry.id);\n this.publish(entry);\n this.scheduleSave();\n return;\n }\n }\n\n private addStep(entry: JournalEntry, input: JournalStepInput): void {\n const steps = this.traces.get(entry.id);\n if (!steps || steps.length > MAX_STEPS) {\n return;\n }\n if (steps.length === MAX_STEPS) {\n steps.push({\n t: Date.now() - entry.ts,\n stage: 'error',\n level: 'warn',\n title: `More than ${MAX_STEPS} steps — the rest is not recorded`,\n });\n return;\n }\n const step: JournalStep = {\n t: Date.now() - entry.ts - (input.ms || 0),\n stage: input.stage,\n title: input.title,\n };\n if (input.ms !== undefined) {\n step.ms = input.ms;\n }\n if (input.level) {\n step.level = input.level;\n }\n if (input.tokens) {\n step.tokens = input.tokens;\n const sum = (entry.tokens ||= { input: 0, output: 0 });\n sum.input += input.tokens.input || 0;\n sum.output += input.tokens.output || 0;\n if (input.tokens.cacheRead) {\n sum.cacheRead = (sum.cacheRead || 0) + input.tokens.cacheRead;\n }\n if (input.tokens.cacheWrite) {\n sum.cacheWrite = (sum.cacheWrite || 0) + input.tokens.cacheWrite;\n }\n }\n if (input.data !== undefined) {\n step.data = snapshot(input.data);\n }\n steps.push(step);\n }\n\n /** Fill in fields of the current request's entry (tier, detail, followUp …). */\n note(fields: Partial<Pick<JournalEntry, 'tier' | 'detail' | 'followUp'>>): void {\n const entry = this.store.getStore();\n if (entry) {\n Object.assign(entry, fields);\n }\n }\n\n /** Record a tool call the model made for the current request. */\n tool(name: string, args: Record<string, unknown>, ms: number, result: unknown, error?: string): void {\n const entry = this.store.getStore();\n if (!entry) {\n return;\n }\n const item: JournalTool = { name, args: args || {}, ms };\n if (error) {\n item.error = error;\n } else {\n let text: string;\n try {\n text = typeof result === 'string' ? result : JSON.stringify(result);\n } catch {\n text = String(result);\n }\n item.result = text.length > RESULT_CHARS ? `${text.slice(0, RESULT_CHARS)}…` : text;\n }\n entry.tools.push(item);\n // Live view: show each tool call as it happens, a cloud round can take seconds.\n this.publish(entry);\n }\n\n /** Record which states a call reads or writes (MCP `get_states`/`set_state`/`set_states`, routines). */\n stateAccess(tool: string, args: Record<string, unknown> | undefined): void {\n const entry = this.store.getStore();\n if (!entry || !args) {\n return;\n }\n if (tool === 'get_states' && Array.isArray(args.ids)) {\n for (const id of args.ids) {\n if (typeof id === 'string' && !entry.read.includes(id)) {\n entry.read.push(id);\n }\n }\n } else if (tool === 'get_state' && typeof args.id === 'string') {\n if (!entry.read.includes(args.id)) {\n entry.read.push(args.id);\n }\n } else if (tool === 'set_state' && typeof args.id === 'string') {\n entry.written.push({ id: args.id, value: args.value });\n } else if (tool === 'set_states' && Array.isArray(args.states)) {\n for (const s of args.states as { id?: unknown; value?: unknown }[]) {\n if (typeof s?.id === 'string') {\n entry.written.push({ id: s.id, value: s.value });\n }\n }\n }\n }\n\n /** Write pending changes now (on unload). */\n async flush(): Promise<void> {\n if (this.saveTimer) {\n clearTimeout(this.saveTimer);\n this.saveTimer = null;\n }\n await this.save();\n }\n\n private publish(entry: JournalEntry): void {\n try {\n this.opts.onChange?.(entry);\n } catch {\n // a listener must never break the answer\n }\n }\n\n private prune(): void {\n const cutoff = Date.now() - this.opts.hours * 3600_000;\n let start = 0;\n while (start < this.entries.length && this.entries[start].ts < cutoff) {\n start++;\n }\n start = Math.max(start, this.entries.length - MAX_ENTRIES);\n if (start > 0) {\n for (const e of this.entries.slice(0, start)) {\n this.traces.delete(e.id);\n this.dirty.delete(e.id);\n }\n this.entries = this.entries.slice(start);\n }\n // Without a file the memory is all there is: keep as many as would be on disk.\n const keep = this.traceDir ? MEMORY_TRACES : MAX_TRACES;\n for (const id of this.traces.keys()) {\n if (this.traces.size <= keep) {\n break;\n }\n // Not written yet, or still being answered: needed until it is on disk.\n if ((!this.traceDir || !this.dirty.has(id)) && this.get(id)?.done !== false) {\n this.traces.delete(id);\n }\n }\n }\n\n private scheduleSave(): void {\n if (!this.opts.file || this.saveTimer) {\n return;\n }\n this.saveTimer = setTimeout(() => {\n this.saveTimer = null;\n void this.save();\n }, SAVE_DELAY_MS);\n this.saveTimer.unref?.();\n }\n\n private async save(): Promise<void> {\n if (!this.opts.file) {\n return;\n }\n try {\n await fs.mkdir(path.dirname(this.opts.file), { recursive: true });\n await fs.writeFile(this.opts.file, JSON.stringify(this.entries.filter(e => e.done)));\n } catch (e) {\n this.opts.log.warn(`Journal could not be saved: ${(e as Error).message}`);\n }\n await this.saveTraces();\n this.prune();\n }\n\n /** Write the traces that changed, and delete the files of requests that are no longer kept. */\n private async saveTraces(): Promise<void> {\n try {\n await fs.mkdir(this.traceDir, { recursive: true });\n for (const id of [...this.dirty]) {\n const steps = this.traces.get(id);\n this.dirty.delete(id);\n if (steps) {\n await fs.writeFile(path.join(this.traceDir, `${id}.json`), JSON.stringify({ id, steps }));\n }\n }\n const keep = new Set(this.entries.slice(-MAX_TRACES).map(e => e.id));\n for (const name of await fs.readdir(this.traceDir)) {\n if (name.endsWith('.json') && !keep.has(name.slice(0, -5))) {\n await fs.rm(path.join(this.traceDir, name), { force: true });\n }\n }\n } catch (e) {\n this.opts.log.warn(`Journal traces could not be saved: ${(e as Error).message}`);\n }\n }\n}\n"]}
|